瀏覽代碼

Merge pull request #783 from nextcloud/notifications

Notifications
Mario Đanić 8 年之前
父節點
當前提交
c050e52c6f
共有 40 個文件被更改,包括 1592 次插入69 次删除
  1. 9 7
      build.gradle
  2. 234 0
      build.gradle.modified
  3. 58 0
      drawable_resources/ic_notification.svg
  4. 58 0
      drawable_resources/ic_notification_light_grey.svg
  5. 1 0
      src/main/AndroidManifest.xml
  6. 13 8
      src/main/java/com/owncloud/android/authentication/AccountAuthenticator.java
  7. 32 0
      src/main/java/com/owncloud/android/db/PreferenceManager.java
  8. 1 1
      src/main/java/com/owncloud/android/files/services/FileDownloader.java
  9. 3 2
      src/main/java/com/owncloud/android/operations/GetServerInfoOperation.java
  10. 4 0
      src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
  11. 10 8
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  12. 286 0
      src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.java
  13. 2 1
      src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java
  14. 1 3
      src/main/java/com/owncloud/android/ui/activity/WhatsNewActivity.java
  15. 1 1
      src/main/java/com/owncloud/android/ui/adapter/FileListListAdapter.java
  16. 129 0
      src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java
  17. 26 0
      src/main/java/com/owncloud/android/ui/events/TokenPushEvent.java
  18. 38 38
      src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
  19. 二進制
      src/main/res/drawable-hdpi/ic_notification.png
  20. 二進制
      src/main/res/drawable-hdpi/ic_notification_light_grey.png
  21. 二進制
      src/main/res/drawable-mdpi/ic_notification.png
  22. 二進制
      src/main/res/drawable-mdpi/ic_notification_light_grey.png
  23. 二進制
      src/main/res/drawable-xhdpi/ic_notification.png
  24. 二進制
      src/main/res/drawable-xhdpi/ic_notification_light_grey.png
  25. 二進制
      src/main/res/drawable-xxhdpi/ic_notification.png
  26. 二進制
      src/main/res/drawable-xxhdpi/ic_notification_light_grey.png
  27. 二進制
      src/main/res/drawable-xxxhdpi/ic_notification.png
  28. 二進制
      src/main/res/drawable-xxxhdpi/ic_notification_light_grey.png
  29. 82 0
      src/main/res/layout/notifications_layout.xml
  30. 5 0
      src/main/res/menu/drawer_menu.xml
  31. 2 0
      src/main/res/values/setup.xml
  32. 7 0
      src/main/res/values/strings.xml
  33. 100 0
      src/modified/AndroidManifest.xml
  34. 41 0
      src/modified/java/com/owncloud/android/authentication/ModifiedAuthenticatorActivity.java
  35. 45 0
      src/modified/java/com/owncloud/android/services/firebase/NCFirebaseInstanceIDService.java
  36. 66 0
      src/modified/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java
  37. 36 0
      src/modified/java/com/owncloud/android/ui/activity/ModifiedFileDisplayActivity.java
  38. 49 0
      src/modified/java/com/owncloud/android/utils/GooglePlayUtils.java
  39. 248 0
      src/modified/java/com/owncloud/android/utils/PushUtils.java
  40. 5 0
      src/modified/res/values/setup.xml

+ 9 - 7
build.gradle

@@ -13,7 +13,7 @@ buildscript {
         }
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:2.3.0'
+        classpath 'com.android.tools.build:gradle:2.3.1'
         classpath 'com.google.gms:google-services:3.0.0'
     }
 }
@@ -37,14 +37,12 @@ ext {
     preDexEnabled = "true".equals(System.getProperty("pre-dex", "true"))
 }
 
-configurations.all {
-    // Check for updates every build
-    resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
-}
-
 repositories {
     jcenter()
     maven { url "https://jitpack.io" }
+    maven {
+        url 'https://oss.sonatype.org/content/repositories/snapshots/'
+    }
 
     flatDir {
         dirs 'libs'
@@ -178,7 +176,7 @@ dependencies {
     compile name: 'touch-image-view'
     compile 'com.android.support:multidex:1.0.1'
 
-    compile 'com.github.nextcloud:android-library:1.0.14'
+    compile 'com.github.nextcloud:android-library:-SNAPSHOT'
     compile "com.android.support:support-v4:${supportLibraryVersion}"
     compile "com.android.support:design:${supportLibraryVersion}"
     compile 'com.jakewharton:disklrucache:2.0.2'
@@ -221,6 +219,10 @@ dependencies {
 
 }
 
+configurations.all {
+    resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
+}
+
 tasks.withType(Test) {
     /// increased logging for tests
     testLogging {

+ 234 - 0
build.gradle.modified

@@ -0,0 +1,234 @@
+// Gradle build file
+//
+// This project was started in Eclipse and later moved to Android Studio. In the transition, both IDEs were supported.
+// Due to this, the files layout is not the usual in new projects created with Android Studio / gradle. This file
+// merges declarations usually split in two separates build.gradle file, one for global settings of the project in
+// its root folder, another one for the app module in subfolder of root.
+
+buildscript {
+    repositories {
+        jcenter()
+        maven {
+            url 'https://oss.sonatype.org/content/repositories/snapshots/'
+        }
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:2.3.1'
+        classpath 'com.google.gms:google-services:3.0.0'
+    }
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'checkstyle'
+apply plugin: 'pmd'
+apply plugin: 'findbugs'
+
+configurations.all {
+    // check for updates every build
+    resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
+}
+
+ext {
+    supportLibraryVersion = '25.0.0'
+
+    travisBuild = System.getenv("TRAVIS") == "true"
+
+    // allows for -Dpre-dex=false to be set :)
+    preDexEnabled = "true".equals(System.getProperty("pre-dex", "true"))
+}
+
+repositories {
+    jcenter()
+    maven { url "https://jitpack.io" }
+    maven {
+        url 'https://oss.sonatype.org/content/repositories/snapshots/'
+    }
+
+    flatDir {
+        dirs 'libs'
+    }
+}
+
+android {
+    lintOptions {
+        abortOnError true
+        lintConfig file("${project.rootDir}/lint.xml")
+        htmlReport true
+        htmlOutput file("$project.buildDir/reports/lint/lint.html")
+    }
+
+    dexOptions {
+        javaMaxHeapSize "4g"
+    }
+
+    compileSdkVersion 25
+    buildToolsVersion '25.0.0'
+
+    defaultConfig {
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+        // arguments to be passed to functional tests
+        testInstrumentationRunnerArgument "TEST_USER", "\"$System.env.OCTEST_APP_USERNAME\""
+        testInstrumentationRunnerArgument "TEST_PASSWORD", "\"$System.env.OCTEST_APP_PASSWORD\""
+        testInstrumentationRunnerArgument "TEST_SERVER_URL", "\"$System.env.OCTEST_SERVER_BASE_URL\""
+
+        multiDexEnabled true
+
+        // adapt structure from Eclipse to Gradle/Android Studio expectations;
+        // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure
+
+        productFlavors {
+            generic {
+                applicationId 'com.nextcloud.client'
+            }
+
+            modified {
+                // structure is:
+                // domain tld
+                // domain name
+                // .client
+                applicationId 'com.custom.client'
+            }
+        }
+
+        configurations {
+            modifiedCompile
+        }
+    }
+
+
+    // adapt structure from Eclipse to Gradle/Android Studio expectations;
+    // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure
+
+    dexOptions {
+        // Skip pre-dexing whe`n running on Travis CI or when disabled via -Dpre-dex=false.
+        preDexLibraries = preDexEnabled && !travisBuild
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_7
+        targetCompatibility JavaVersion.VERSION_1_7
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    packagingOptions {
+        exclude 'META-INF/LICENSE.txt'
+    }
+
+    task checkstyle(type: Checkstyle) {
+        configFile = file("${rootProject.projectDir}/checkstyle.xml")
+        configProperties.checkstyleSuppressionsPath = file("${project.rootDir}/config/quality/checkstyle/suppressions.xml").absolutePath
+        source 'src'
+        include '**/*.java'
+        exclude '**/gen/**'
+        classpath = files()
+    }
+
+    task pmd(type: Pmd) {
+        ruleSetFiles = files("${project.rootDir}/pmd-ruleset.xml")
+        ignoreFailures = false
+        ruleSets = []
+
+        source 'src'
+        include '**/*.java'
+        exclude '**/gen/**'
+
+        reports {
+            xml.enabled = false
+            html.enabled = true
+            xml {
+                destination "$project.buildDir/reports/pmd/pmd.xml"
+            }
+            html {
+                destination "$project.buildDir/reports/pmd/pmd.html"
+            }
+        }
+    }
+
+    task findbugs(type: FindBugs) {
+        ignoreFailures = false
+        effort = "max"
+        reportLevel = "high"
+        classes = files("$project.buildDir/intermediates/classes")
+        excludeFilter = new File("${project.rootDir}/findbugs-filter.xml")
+        source 'src'
+        include '**/*.java'
+        exclude '**/gen/**'
+
+        reports {
+            xml.enabled = false
+            html.enabled = true
+            html {
+                destination "$project.buildDir/reports/findbugs/findbugs.html"
+            }
+        }
+        classpath = files()
+    }
+    check.dependsOn 'checkstyle', 'findbugs', 'pmd', 'lint'
+
+}
+
+dependencies {
+    /// dependencies for app building
+    compile name: 'touch-image-view'
+    compile 'com.android.support:multidex:1.0.1'
+
+    compile 'com.github.nextcloud:android-library:notifications-push-SNAPSHOT'
+    compile "com.android.support:support-v4:${supportLibraryVersion}"
+    compile "com.android.support:design:${supportLibraryVersion}"
+    compile 'com.jakewharton:disklrucache:2.0.2'
+    compile "com.android.support:appcompat-v7:${supportLibraryVersion}"
+    compile "com.android.support:cardview-v7:${supportLibraryVersion}"
+    compile 'com.getbase:floatingactionbutton:1.10.1'
+    compile 'com.google.code.findbugs:annotations:2.0.1'
+    compile group: 'commons-io', name: 'commons-io', version: '2.4'
+    compile 'com.github.evernote:android-job:v1.1.9'
+    compile 'com.jakewharton:butterknife:8.4.0'
+    annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
+    compile 'org.greenrobot:eventbus:3.0.0'
+
+    compile 'com.google.android.gms:play-services:10.2.1'
+
+    compile 'org.parceler:parceler-api:1.1.6'
+    annotationProcessor 'org.parceler:parceler:1.1.6'
+
+    compile 'com.github.bumptech.glide:glide:3.7.0'
+    compile 'com.caverock:androidsvg:1.2.1'
+    /// dependencies for local unit tests
+    testCompile 'junit:junit:4.12'
+    testCompile 'org.mockito:mockito-core:1.10.19'
+
+    /// dependencies for instrumented tests
+    // JUnit4 Rules
+    androidTestCompile 'com.android.support.test:rules:0.5'
+
+    // Android JUnit Runner
+    androidTestCompile 'com.android.support.test:runner:0.5'
+
+    // Android Annotation Support
+    androidTestCompile "com.android.support:support-annotations:${supportLibraryVersion}"
+
+    // Espresso core
+    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
+
+    // UIAutomator - for cross-app UI tests, and to grant screen is turned on in Espresso tests
+    //androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
+    // fix conflict in dependencies; see http://g.co/androidstudio/app-test-app-conflict for details
+    //androidTestCompile "com.android.support:support-annotations:${supportLibraryVersion}"
+}
+
+configurations.all {
+    resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
+}
+
+tasks.withType(Test) {
+    /// increased logging for tests
+    testLogging {
+        events "passed", "skipped", "failed"
+    }
+}
+
+apply plugin: 'com.google.gms.google-services'

+ 58 - 0
drawable_resources/ic_notification.svg

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   width="16"
+   version="1.1"
+   viewBox="0 0 16 16"
+   id="svg4"
+   sodipodi:docname="ic_notification.svg"
+   inkscape:version="0.92.1 r15371"
+   inkscape:export-filename="C:\DEV\src\Android\Nextcloud\notifications\src\main\res\drawable-mdpi\ic_notification.png"
+   inkscape:export-xdpi="144"
+   inkscape:export-ydpi="144">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1005"
+     id="namedview6"
+     showgrid="false"
+     inkscape:zoom="14.75"
+     inkscape:cx="8"
+     inkscape:cy="8"
+     inkscape:window-x="-9"
+     inkscape:window-y="-9"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4" />
+  <path
+     d="m8 2c-0.5523 0-1 0.4477-1 1 0 0.0472 0.021 0.0873 0.0273 0.1328-1.7366 0.4362-3.0273 1.9953-3.0273 3.8672v2l-1 1v1h10v-1l-1-1v-2c0-1.8719-1.291-3.431-3.0273-3.8672 0.0063-0.0455 0.0273-0.0856 0.0273-0.1328 0-0.5523-0.4477-1-1-1zm-2 10c0 1.1046 0.8954 2 2 2s2-0.8954 2-2z"
+     fill="#fff"
+     id="path2"
+     style="fill:#757575;fill-opacity:1" />
+</svg>

+ 58 - 0
drawable_resources/ic_notification_light_grey.svg

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   width="16"
+   version="1.1"
+   viewBox="0 0 16 16"
+   id="svg4"
+   sodipodi:docname="ic_notification_light_grey.svg"
+   inkscape:version="0.92.1 r15371"
+   inkscape:export-filename="C:\DEV\src\Android\Nextcloud\notifications\src\main\res\drawable-mdpi\ic_notification_light_grey.png"
+   inkscape:export-xdpi="432"
+   inkscape:export-ydpi="432">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1005"
+     id="namedview6"
+     showgrid="false"
+     inkscape:zoom="14.75"
+     inkscape:cx="-10.20339"
+     inkscape:cy="8"
+     inkscape:window-x="-9"
+     inkscape:window-y="-9"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4" />
+  <path
+     d="m8 2c-0.5523 0-1 0.4477-1 1 0 0.0472 0.021 0.0873 0.0273 0.1328-1.7366 0.4362-3.0273 1.9953-3.0273 3.8672v2l-1 1v1h10v-1l-1-1v-2c0-1.8719-1.291-3.431-3.0273-3.8672 0.0063-0.0455 0.0273-0.0856 0.0273-0.1328 0-0.5523-0.4477-1-1-1zm-2 10c0 1.1046 0.8954 2 2 2s2-0.8954 2-2z"
+     fill="#fff"
+     id="path2"
+     style="fill:#000000;fill-opacity:1;opacity:0.5" />
+</svg>

+ 1 - 0
src/main/AndroidManifest.xml

@@ -77,6 +77,7 @@
         </activity>
         <activity android:name=".ui.activity.ManageAccountsActivity" />
         <activity android:name=".ui.activity.UserInfoActivity" />
+        <activity android:name=".ui.activity.NotificationsActivity"/>
         <activity android:name=".ui.activity.ParticipateActivity" />
         <activity android:name=".ui.activity.ActivitiesListActivity"/>
         <activity android:name=".ui.activity.FolderSyncActivity" />

+ 13 - 8
src/main/java/com/owncloud/android/authentication/AccountAuthenticator.java

@@ -21,16 +21,19 @@
 
 package com.owncloud.android.authentication;
 
-import com.owncloud.android.MainApp;
-import com.owncloud.android.R;
-
-import android.accounts.*;
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.accounts.NetworkErrorException;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.Handler;
 import android.widget.Toast;
 
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
 import com.owncloud.android.lib.common.accounts.AccountTypeUtils;
 import com.owncloud.android.lib.common.utils.Log_OC;
 
@@ -90,8 +93,8 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
                         + e.getMessage(), e);
                 return e.getFailureBundle();
             }
-            
-            final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
+
+            Intent intent = new Intent(mContext, AuthenticatorActivity.class);
             intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
             intent.putExtra(KEY_AUTH_TOKEN_TYPE, authTokenType);
             intent.putExtra(KEY_REQUIRED_FEATURES, requiredFeatures);
@@ -134,6 +137,7 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
             Log_OC.e(TAG, "Failed to validate account type " + account.type + ": " + e.getMessage(), e);
             return e.getFailureBundle();
         }
+
         Intent intent = new Intent(mContext, AuthenticatorActivity.class);
         intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
                 response);
@@ -185,7 +189,7 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
         }
         
         /// if not stored, return Intent to access the AuthenticatorActivity and UPDATE the token for the account
-        final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
+        Intent intent = new Intent(mContext, AuthenticatorActivity.class);
         intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
         intent.putExtra(KEY_AUTH_TOKEN_TYPE, authTokenType);
         intent.putExtra(KEY_LOGIN_OPTIONS, options);
@@ -215,7 +219,8 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
     public Bundle updateCredentials(AccountAuthenticatorResponse response,
             Account account, String authTokenType, Bundle options)
             throws NetworkErrorException {
-        final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
+
+        Intent intent = new Intent(mContext, AuthenticatorActivity.class);
         intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
         intent.putExtra(KEY_ACCOUNT, account);
         intent.putExtra(KEY_AUTH_TOKEN_TYPE, authTokenType);

+ 32 - 0
src/main/java/com/owncloud/android/db/PreferenceManager.java

@@ -46,7 +46,33 @@ public abstract class PreferenceManager {
     private static final String PREF__INSTANT_VIDEO_UPLOAD_PATH_USE_SUBFOLDERS = "instant_video_upload_path_use_subfolders";
     private static final String PREF__LEGACY_CLEAN = "legacyClean";
     private static final String PREF__AUTO_UPLOAD_UPDATE_PATH = "autoUploadPathUpdate";
+    private static final String PREF__PUSH_TOKEN = "pushToken";
+    private static final String PREF__PUSH_TOKEN_UPDATE_TIME = "pushTokenLastUpdated";
+    private static final String PREF__PUSH_TOKEN_LAST_REGISTRATION_TIME = "pushTokenLastSent";
 
+    public static void setPushTokenLastSentTime(Context context, long time) {
+        saveLongPreference(context, PREF__PUSH_TOKEN_LAST_REGISTRATION_TIME, time);
+    }
+
+    public static long getPushTokenLastSentTime(Context context) {
+        return getDefaultSharedPreferences(context).getLong(PREF__PUSH_TOKEN_LAST_REGISTRATION_TIME, -1);
+    }
+
+    public static void setPushToken(Context context, String pushToken) {
+        saveStringPreference(context, PREF__PUSH_TOKEN, pushToken);
+    }
+
+    public static String getPushToken(Context context) {
+        return getDefaultSharedPreferences(context).getString(PREF__PUSH_TOKEN, "");
+    }
+
+    public static void setPushTokenUpdateTime(Context context, long time) {
+        saveLongPreference(context, PREF__PUSH_TOKEN_UPDATE_TIME, time);
+    }
+
+    public static long getPushTokenUpdateTime(Context context) {
+        return getDefaultSharedPreferences(context).getLong(PREF__PUSH_TOKEN_UPDATE_TIME, -1);
+    }
 
     public static boolean instantPictureUploadEnabled(Context context) {
         return getDefaultSharedPreferences(context).getBoolean(PREF__INSTANT_UPLOADING, false);
@@ -267,6 +293,12 @@ public abstract class PreferenceManager {
         appPreferences.apply();
     }
 
+    private static void saveLongPreference(Context context, String key, long value) {
+        SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
+        appPreferences.putLong(key, value);
+        appPreferences.apply();
+    }
+
     public static SharedPreferences getDefaultSharedPreferences(Context context) {
         return android.preference.PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
     }

+ 1 - 1
src/main/java/com/owncloud/android/files/services/FileDownloader.java

@@ -51,10 +51,10 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.FileUtils;
-import com.owncloud.android.ui.notifications.NotificationUtils;
 import com.owncloud.android.operations.DownloadFileOperation;
 import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
+import com.owncloud.android.ui.notifications.NotificationUtils;
 import com.owncloud.android.ui.preview.PreviewImageActivity;
 import com.owncloud.android.ui.preview.PreviewImageFragment;
 import com.owncloud.android.utils.ErrorMessageAdapter;

+ 3 - 2
src/main/java/com/owncloud/android/operations/GetServerInfoOperation.java

@@ -21,7 +21,7 @@
 
 package com.owncloud.android.operations;
 
-import java.util.ArrayList;
+import android.content.Context;
 
 import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.lib.common.OwnCloudClient;
@@ -33,7 +33,7 @@ import com.owncloud.android.lib.resources.status.GetRemoteStatusOperation;
 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
 import com.owncloud.android.operations.DetectAuthenticationMethodOperation.AuthenticationMethod;
 
-import android.content.Context;
+import java.util.ArrayList;
 
 /**
  * Get basic information from an ownCloud server given its URL.
@@ -78,6 +78,7 @@ public class GetServerInfoOperation extends RemoteOperation {
 	    
 	    // first: check the status of the server (including its version)
 	    GetRemoteStatusOperation getStatus = new GetRemoteStatusOperation(mContext);
+
 	    RemoteOperationResult result = getStatus.execute(client);
 	    
         if (result.isSuccess()) {

+ 4 - 0
src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -411,6 +411,10 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
                 activityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                 startActivity(activityIntent);
                 break;
+            case R.id.nav_notifications:
+                Intent notificationsIntent = new Intent(getApplicationContext(), NotificationsActivity.class);
+                startActivity(notificationsIntent);
+                break;
             case R.id.nav_folder_sync:
                 Intent folderSyncIntent = new Intent(getApplicationContext(), FolderSyncActivity.class);
                 startActivity(folderSyncIntent);

+ 10 - 8
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -82,6 +82,7 @@ import com.owncloud.android.operations.UploadFileOperation;
 import com.owncloud.android.services.observer.FileObserverService;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
 import com.owncloud.android.ui.dialog.SortingOrderDialogFragment;
+import com.owncloud.android.ui.events.TokenPushEvent;
 import com.owncloud.android.ui.fragment.ExtendedListFragment;
 import com.owncloud.android.ui.fragment.FileDetailFragment;
 import com.owncloud.android.ui.fragment.FileFragment;
@@ -98,6 +99,8 @@ import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.ErrorMessageAdapter;
 import com.owncloud.android.utils.PermissionUtil;
 
+import org.greenrobot.eventbus.EventBus;
+
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -172,11 +175,9 @@ public class FileDisplayActivity extends HookActivity
 
         /// Load of saved instance state
         if (savedInstanceState != null) {
-            mWaitingToPreview = savedInstanceState.getParcelable(
-                    FileDisplayActivity.KEY_WAITING_TO_PREVIEW);
+            mWaitingToPreview = savedInstanceState.getParcelable(FileDisplayActivity.KEY_WAITING_TO_PREVIEW);
             mSyncInProgress = savedInstanceState.getBoolean(KEY_SYNC_IN_PROGRESS);
-            mWaitingToSend = savedInstanceState.getParcelable(
-                    FileDisplayActivity.KEY_WAITING_TO_SEND);
+            mWaitingToSend = savedInstanceState.getParcelable(FileDisplayActivity.KEY_WAITING_TO_SEND);
             searchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY);
         } else {
             mWaitingToPreview = null;
@@ -247,11 +248,11 @@ public class FileDisplayActivity extends HookActivity
             }
         }
 
-        if (getIntent().getParcelableExtra(OCFileListFragment.SEARCH_EVENT) != null){
+        if (getIntent().getParcelableExtra(OCFileListFragment.SEARCH_EVENT) != null) {
             switchToSearchFragment();
 
             int menuId = getIntent().getIntExtra(DRAWER_MENU_ID, -1);
-            if (menuId != -1){
+            if (menuId != -1) {
                 setupDrawer(menuId);
             }
         } else if (savedInstanceState == null) {
@@ -323,6 +324,7 @@ public class FileDisplayActivity extends HookActivity
                 if (grantResults.length > 0
                         && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                     // permission was granted
+                    EventBus.getDefault().post(new TokenPushEvent());
                     refreshList(true);
                     // toggle on is save since this is the only scenario this code gets accessed
                 } else {
@@ -913,7 +915,7 @@ public class FileDisplayActivity extends HookActivity
     /**
      * Request the operation for moving the file/folder from one path to another
      *
-     * @param data       Intent received
+     * @param data Intent received
      */
     private void requestMoveOperation(Intent data) {
         OCFile folderToMoveAt = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER);
@@ -924,7 +926,7 @@ public class FileDisplayActivity extends HookActivity
     /**
      * Request the operation for copying the file/folder from one path to another
      *
-     * @param data       Intent received
+     * @param data Intent received
      */
     private void requestCopyOperation(Intent data) {
         OCFile folderToMoveAt = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER);

+ 286 - 0
src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.java

@@ -0,0 +1,286 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * @author Mario Danic
+ * Copyright (C) 2017 Andy Scherzinger
+ * Copyright (C) 2017 Mario Danic
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.activity;
+
+import android.accounts.Account;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.support.v7.widget.DividerItemDecoration;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.authentication.AccountUtils;
+import com.owncloud.android.lib.common.OwnCloudAccount;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
+import com.owncloud.android.lib.common.operations.RemoteOperation;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.lib.resources.notifications.GetRemoteNotificationsOperation;
+import com.owncloud.android.lib.resources.notifications.models.Notification;
+import com.owncloud.android.ui.adapter.NotificationListAdapter;
+
+import java.io.IOException;
+import java.util.List;
+
+import butterknife.BindString;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.Unbinder;
+
+/**
+ * Activity displaying all server side stored activity items.
+ */
+public class NotificationsActivity extends FileActivity {
+
+    private static final String TAG = NotificationsActivity.class.getSimpleName();
+
+    @BindView(R.id.empty_list_view)
+    public LinearLayout emptyContentContainer;
+
+    @BindView(R.id.swipe_containing_list)
+    public SwipeRefreshLayout swipeListRefreshLayout;
+
+    @BindView(R.id.swipe_containing_empty)
+    public SwipeRefreshLayout swipeEmptyListRefreshLayout;
+
+    @BindView(R.id.empty_list_view_text)
+    public TextView emptyContentMessage;
+
+    @BindView(R.id.empty_list_view_headline)
+    public TextView emptyContentHeadline;
+
+    @BindView(R.id.empty_list_icon)
+    public ImageView emptyContentIcon;
+
+    @BindView(R.id.empty_list_progress)
+    public ProgressBar emptyContentProgressBar;
+
+    @BindView(android.R.id.list)
+    public RecyclerView recyclerView;
+
+    @BindString(R.string.notifications_no_results_headline)
+    public String noResultsHeadline;
+
+    @BindString(R.string.notifications_no_results_message)
+    public String noResultsMessage;
+
+    private Unbinder unbinder;
+
+    private NotificationListAdapter adapter;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        Log_OC.v(TAG, "onCreate() start");
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.notifications_layout);
+        unbinder = ButterKnife.bind(this);
+
+        // setup toolbar
+        setupToolbar();
+
+        // setup drawer
+        setupDrawer(R.id.nav_notifications);
+        getSupportActionBar().setTitle(getString(R.string.drawer_item_notifications));
+
+        swipeListRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
+            @Override
+            public void onRefresh() {
+                setLoadingMessage();
+                fetchAndSetData();
+            }
+        });
+
+        swipeEmptyListRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
+            @Override
+            public void onRefresh() {
+                setLoadingMessage();
+                fetchAndSetData();
+
+            }
+        });
+
+        setupContent();
+    }
+
+    public void onDestroy() {
+        super.onDestroy();
+        unbinder.unbind();
+    }
+
+    @Override
+    public void showFiles(boolean onDeviceOnly) {
+        super.showFiles(onDeviceOnly);
+        Intent i = new Intent(getApplicationContext(), FileDisplayActivity.class);
+        i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        startActivity(i);
+    }
+
+    /**
+     * sets up the UI elements and loads all activity items.
+     */
+    private void setupContent() {
+        emptyContentIcon.setImageResource(R.drawable.ic_notification_light_grey);
+        setLoadingMessage();
+
+        adapter = new NotificationListAdapter(this);
+        recyclerView.setAdapter(adapter);
+
+        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
+        DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(
+                recyclerView.getContext(),
+                layoutManager.getOrientation()
+        );
+
+        recyclerView.setLayoutManager(layoutManager);
+        recyclerView.addItemDecoration(dividerItemDecoration);
+
+        fetchAndSetData();
+    }
+
+    private void populateList(List<Notification> notifications) {
+        adapter.setNotificationItems(notifications);
+    }
+
+    private void fetchAndSetData() {
+        final Account currentAccount = AccountUtils.getCurrentOwnCloudAccount(MainApp.getAppContext());
+        final Context context = MainApp.getAppContext();
+
+        Thread t = new Thread(new Runnable() {
+            public void run() {
+                OwnCloudAccount ocAccount;
+                try {
+                    ocAccount = new OwnCloudAccount(currentAccount, context);
+                    OwnCloudClient mClient = OwnCloudClientManagerFactory.getDefaultSingleton().
+                            getClientFor(ocAccount, MainApp.getAppContext());
+                    mClient.setOwnCloudVersion(AccountUtils.getServerVersion(currentAccount));
+
+                    RemoteOperation getRemoteNotificationOperation = new GetRemoteNotificationsOperation();
+                    final RemoteOperationResult result = getRemoteNotificationOperation.execute(mClient);
+
+                    if (result.isSuccess() && result.getNotificationData() != null) {
+                        final List<Notification> notifications = result.getNotificationData();
+
+                        runOnUiThread(new Runnable() {
+                            @Override
+                            public void run() {
+                                if (notifications.size() > 0) {
+                                    populateList(notifications);
+                                    swipeEmptyListRefreshLayout.setVisibility(View.GONE);
+                                    swipeListRefreshLayout.setVisibility(View.VISIBLE);
+                                } else {
+                                    setEmptyContent(noResultsHeadline, noResultsMessage);
+                                }
+                            }
+                        });
+                    } else {
+                        Log_OC.d(TAG, result.getLogMessage());
+                        // show error
+                        runOnUiThread(new Runnable() {
+                            @Override
+                            public void run() {
+                                setEmptyContent(noResultsHeadline, result.getLogMessage());
+                            }
+                        });
+                    }
+
+                    hideRefreshLayoutLoader();
+                } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
+                    Log_OC.e(TAG, "Account not found", e);
+                } catch (IOException e) {
+                    Log_OC.e(TAG, "IO error", e);
+                } catch (OperationCanceledException e) {
+                    Log_OC.e(TAG, "Operation has been canceled", e);
+                } catch (AuthenticatorException e) {
+                    Log_OC.e(TAG, "Authentication Exception", e);
+                }
+            }
+        });
+
+        t.start();
+
+    }
+
+    private void hideRefreshLayoutLoader() {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                swipeListRefreshLayout.setRefreshing(false);
+                swipeEmptyListRefreshLayout.setRefreshing(false);
+            }
+        });
+    }
+
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        boolean retval = true;
+
+        switch (item.getItemId()) {
+            case android.R.id.home:
+                if (isDrawerOpen()) {
+                    closeDrawer();
+                } else {
+                    openDrawer();
+                }
+                break;
+
+            default:
+                retval = super.onOptionsItemSelected(item);
+                break;
+        }
+
+        return retval;
+    }
+
+    private void setLoadingMessage() {
+        emptyContentHeadline.setText(R.string.file_list_loading);
+        emptyContentMessage.setText("");
+
+        emptyContentIcon.setVisibility(View.GONE);
+        emptyContentProgressBar.setVisibility(View.VISIBLE);
+    }
+
+    private void setEmptyContent(String headline, String message) {
+        if (emptyContentContainer != null && emptyContentMessage != null) {
+            emptyContentHeadline.setText(headline);
+            emptyContentMessage.setText(message);
+
+            emptyContentProgressBar.setVisibility(View.GONE);
+            emptyContentIcon.setVisibility(View.VISIBLE);
+        }
+    }
+}

+ 2 - 1
src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java

@@ -284,7 +284,8 @@ public class UserInfoActivity extends FileActivity {
     }
 
     private void changeAccountPassword(Account account) {
-        Intent updateAccountCredentials = new Intent(UserInfoActivity.this, AuthenticatorActivity.class);
+        // let the user update credentials with one click
+        Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class);
         updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, account);
         updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACTION,
                 AuthenticatorActivity.ACTION_UPDATE_TOKEN);

+ 1 - 3
src/main/java/com/owncloud/android/ui/activity/WhatsNewActivity.java

@@ -253,9 +253,8 @@ public class WhatsNewActivity extends FragmentActivity implements ViewPager.OnPa
 
             return v;
         }
-
-
     }
+
     private final class FeaturesViewAdapter extends FragmentPagerAdapter {
 
         private FeatureItem[] mFeatures;
@@ -318,5 +317,4 @@ public class WhatsNewActivity extends FragmentActivity implements ViewPager.OnPa
             return v;
         }
     }
-
 }

+ 1 - 1
src/main/java/com/owncloud/android/ui/adapter/FileListListAdapter.java

@@ -116,7 +116,7 @@ public class FileListListAdapter extends BaseAdapter {
             OCFileListFragmentInterface OCFileListFragmentInterface,
             FileDataStorageManager fileDataStorageManager
     ) {
-    this(justFolders, context, transferServiceGetter, OCFileListFragmentInterface);
+        this(justFolders, context, transferServiceGetter, OCFileListFragmentInterface);
         mStorageManager = fileDataStorageManager;
     }
 

+ 129 - 0
src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java

@@ -0,0 +1,129 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2016 ownCloud Inc.
+ * <p/>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ * <p/>
+ * 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.
+ * <p/>
+ * 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.owncloud.android.ui.adapter;
+
+import android.content.Context;
+import android.graphics.drawable.PictureDrawable;
+import android.net.Uri;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.bumptech.glide.GenericRequestBuilder;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.load.model.StreamEncoder;
+import com.bumptech.glide.load.resource.file.FileToStreamDecoder;
+import com.caverock.androidsvg.SVG;
+import com.owncloud.android.R;
+import com.owncloud.android.lib.resources.notifications.models.Notification;
+import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.svg.SvgDecoder;
+import com.owncloud.android.utils.svg.SvgDrawableTranscoder;
+import com.owncloud.android.utils.svg.SvgSoftwareLayerSetter;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This Adapter populates a ListView with all notifications for an account within the app.
+ */
+public class NotificationListAdapter extends RecyclerView.Adapter<NotificationListAdapter.NotificationViewHolder> {
+    private List<Notification> mValues;
+    private Context context;
+
+    public NotificationListAdapter(Context context) {
+        this.mValues = new ArrayList<>();
+        this.context = context;
+    }
+
+    public void setNotificationItems(List<Notification> notificationItems) {
+        mValues.clear();
+        mValues.addAll(notificationItems);
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public NotificationViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity_list_item, parent, false);
+        return new NotificationViewHolder(v);
+    }
+
+    @Override
+    public void onBindViewHolder(NotificationViewHolder holder, int position) {
+        Notification notification = mValues.get(position);
+        holder.dateTime.setText(DisplayUtils.getRelativeTimestamp(context, notification.getDatetime().getTime()));
+        holder.subject.setText(notification.getSubject());
+        holder.message.setText(notification.getMessage());
+
+        // Todo set proper action icon (to be clarified how to pick)
+        if (!TextUtils.isEmpty(notification.getIcon())) {
+            downloadIcon(notification.getIcon(), holder.activityIcon);
+        }
+
+    }
+
+    private void downloadIcon(String icon, ImageView itemViewType) {
+        GenericRequestBuilder<Uri, InputStream, SVG, PictureDrawable> requestBuilder = Glide.with(context)
+                .using(Glide.buildStreamModelLoader(Uri.class, context), InputStream.class)
+                .from(Uri.class)
+                .as(SVG.class)
+                .transcode(new SvgDrawableTranscoder(), PictureDrawable.class)
+                .sourceEncoder(new StreamEncoder())
+                .cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder()))
+                .decoder(new SvgDecoder())
+                .placeholder(R.drawable.ic_notification)
+                .error(R.drawable.ic_notification)
+                .animate(android.R.anim.fade_in)
+                .listener(new SvgSoftwareLayerSetter<Uri>());
+
+
+        Uri uri = Uri.parse(icon);
+        requestBuilder
+                .diskCacheStrategy(DiskCacheStrategy.SOURCE)
+                .load(uri)
+                .into(itemViewType);
+    }
+
+    @Override
+    public int getItemCount() {
+        return mValues.size();
+    }
+
+    static class NotificationViewHolder extends RecyclerView.ViewHolder {
+        private final ImageView activityIcon;
+        private final TextView subject;
+        private final TextView message;
+        private final TextView dateTime;
+
+        private NotificationViewHolder(View itemView) {
+            super(itemView);
+            activityIcon = (ImageView) itemView.findViewById(R.id.activity_icon);
+            subject = (TextView) itemView.findViewById(R.id.activity_subject);
+            message = (TextView) itemView.findViewById(R.id.activity_message);
+            dateTime = (TextView) itemView.findViewById(R.id.activity_datetime);
+        }
+    }
+}

+ 26 - 0
src/main/java/com/owncloud/android/ui/events/TokenPushEvent.java

@@ -0,0 +1,26 @@
+/**
+ * Nextcloud Android client 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 Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.events;
+
+/**
+ * Event to send push token where it belongs
+ */
+public class TokenPushEvent {
+}

+ 38 - 38
src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -1332,48 +1332,48 @@ public class OCFileListFragment extends ExtendedListFragment implements OCFileLi
 
         task.execute(true);
 
-            if (event.getSearchType().equals(SearchOperation.SearchType.FILE_SEARCH)) {
-                setEmptyListMessage(SearchType.FILE_SEARCH);
-
-            } else if (event.getSearchType().equals(SearchOperation.SearchType.CONTENT_TYPE_SEARCH)) {
-                if (event.getSearchQuery().equals("image/%")) {
-                    setEmptyListMessage(SearchType.PHOTO_SEARCH);
-                    menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT;
-                } else if (event.getSearchQuery().equals("video/%")) {
-                    setEmptyListMessage(SearchType.VIDEO_SEARCH);
-                    menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SEARCH;
-                }
-            } else if (event.getSearchType().equals(SearchOperation.SearchType.FAVORITE_SEARCH)) {
-                setEmptyListMessage(SearchType.FAVORITE_SEARCH);
-                menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SORT;
-            } else if (event.getSearchType().equals(SearchOperation.SearchType.RECENTLY_ADDED_SEARCH)) {
-                setEmptyListMessage(SearchType.RECENTLY_ADDED_SEARCH);
-                menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SORT;
-            } else if (event.getSearchType().equals(SearchOperation.SearchType.RECENTLY_MODIFIED_SEARCH)) {
-                setEmptyListMessage(SearchType.RECENTLY_MODIFIED_SEARCH);
-                menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SORT;
-            } else if (event.getSearchType().equals(SearchOperation.SearchType.SHARED_SEARCH)) {
-                setEmptyListMessage(SearchType.SHARED_FILTER);
+        if (event.getSearchType().equals(SearchOperation.SearchType.FILE_SEARCH)) {
+            setEmptyListMessage(SearchType.FILE_SEARCH);
+
+        } else if (event.getSearchType().equals(SearchOperation.SearchType.CONTENT_TYPE_SEARCH)) {
+            if (event.getSearchQuery().equals("image/%")) {
+                setEmptyListMessage(SearchType.PHOTO_SEARCH);
+                menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT;
+            } else if (event.getSearchQuery().equals("video/%")) {
+                setEmptyListMessage(SearchType.VIDEO_SEARCH);
                 menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SEARCH;
             }
+        } else if (event.getSearchType().equals(SearchOperation.SearchType.FAVORITE_SEARCH)) {
+            setEmptyListMessage(SearchType.FAVORITE_SEARCH);
+            menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SORT;
+        } else if (event.getSearchType().equals(SearchOperation.SearchType.RECENTLY_ADDED_SEARCH)) {
+            setEmptyListMessage(SearchType.RECENTLY_ADDED_SEARCH);
+            menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SORT;
+        } else if (event.getSearchType().equals(SearchOperation.SearchType.RECENTLY_MODIFIED_SEARCH)) {
+            setEmptyListMessage(SearchType.RECENTLY_MODIFIED_SEARCH);
+            menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SORT;
+        } else if (event.getSearchType().equals(SearchOperation.SearchType.SHARED_SEARCH)) {
+            setEmptyListMessage(SearchType.SHARED_FILTER);
+            menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SEARCH;
+        }
 
-            if (!currentSearchType.equals(SearchType.FILE_SEARCH) && getActivity() != null) {
-                getActivity().invalidateOptionsMenu();
-            }
+        if (!currentSearchType.equals(SearchType.FILE_SEARCH) && getActivity() != null) {
+            getActivity().invalidateOptionsMenu();
+        }
 
-            if (currentSearchType.equals(SearchType.PHOTO_SEARCH)) {
-                new Handler(Looper.getMainLooper()).post(new Runnable() {
-                    @Override
-                    public void run() {
-                        switchToGridView();
-                    }
-                });
-            } else if (currentSearchType.equals(SearchType.NO_SEARCH) || currentSearchType.equals(
-                    SearchType.REGULAR_FILTER)) {
-                new Handler(Looper.getMainLooper()).post(switchViewsRunnable);
-            } else {
-                new Handler(Looper.getMainLooper()).post(switchViewsRunnable);
-            }
+        if (currentSearchType.equals(SearchType.PHOTO_SEARCH)) {
+            new Handler(Looper.getMainLooper()).post(new Runnable() {
+                @Override
+                public void run() {
+                    switchToGridView();
+                }
+            });
+        } else if (currentSearchType.equals(SearchType.NO_SEARCH) || currentSearchType.equals(
+                SearchType.REGULAR_FILTER)) {
+            new Handler(Looper.getMainLooper()).post(switchViewsRunnable);
+        } else {
+            new Handler(Looper.getMainLooper()).post(switchViewsRunnable);
+        }
     }
 
     @Override

二進制
src/main/res/drawable-hdpi/ic_notification.png


二進制
src/main/res/drawable-hdpi/ic_notification_light_grey.png


二進制
src/main/res/drawable-mdpi/ic_notification.png


二進制
src/main/res/drawable-mdpi/ic_notification_light_grey.png


二進制
src/main/res/drawable-xhdpi/ic_notification.png


二進制
src/main/res/drawable-xhdpi/ic_notification_light_grey.png


二進制
src/main/res/drawable-xxhdpi/ic_notification.png


二進制
src/main/res/drawable-xxhdpi/ic_notification_light_grey.png


二進制
src/main/res/drawable-xxxhdpi/ic_notification.png


二進制
src/main/res/drawable-xxxhdpi/ic_notification_light_grey.png


+ 82 - 0
src/main/res/layout/notifications_layout.xml

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2017 Andy Scherzinger
+  Copyright (C) 2017 Mario Danic
+
+  This program is free software; you can redistribute it and/or
+  modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  License as published by the Free Software Foundation; either
+  version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+
+  You should have received a copy of the GNU Affero General Public
+  License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                                        android:id="@+id/drawer_layout"
+                                        android:layout_width="match_parent"
+                                        android:layout_height="match_parent"
+                                        android:clickable="true"
+                                        android:fitsSystemWindows="true">
+
+    <!-- The main content view -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <include
+            layout="@layout/toolbar_standard"/>
+
+        <FrameLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <android.support.v4.widget.SwipeRefreshLayout
+                android:id="@+id/swipe_containing_list"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:footerDividersEnabled="false"
+                android:visibility="visible">
+
+                <android.support.v7.widget.RecyclerView
+                    android:id="@android:id/list"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:layout_marginBottom="-3dp"
+                    android:layout_marginLeft="-3dp"
+                    android:layout_marginRight="-3dp"
+                    android:clipToPadding="false"
+                    android:scrollbarStyle="outsideOverlay"
+                    android:scrollbars="vertical"
+                    android:visibility="visible"/>
+
+            </android.support.v4.widget.SwipeRefreshLayout>
+
+            <android.support.v4.widget.SwipeRefreshLayout
+                android:id="@+id/swipe_containing_empty"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:footerDividersEnabled="false"
+                android:visibility="visible">
+
+                <include layout="@layout/empty_list"/>
+            </android.support.v4.widget.SwipeRefreshLayout>
+
+        </FrameLayout>
+
+    </LinearLayout>
+
+    <include
+        layout="@layout/drawer"
+        android:layout_width="240dp"
+        android:layout_height="match_parent"
+        android:layout_gravity="start"/>
+
+</android.support.v4.widget.DrawerLayout>

+ 5 - 0
src/main/res/menu/drawer_menu.xml

@@ -73,6 +73,11 @@
             android:orderInCategory="0"
             android:title="@string/drawer_item_recently_modified"
             android:visible="false"/>
+        <item
+            android:orderInCategory="0"
+            android:id="@+id/nav_notifications"
+            android:icon="@drawable/ic_notification"
+            android:title="@string/drawer_item_notifications"/>
         <item
             android:orderInCategory="0"
             android:id="@+id/nav_folder_sync"

+ 2 - 0
src/main/res/values/setup.xml

@@ -122,6 +122,8 @@
     <!-- Files becomes Home -->
     <bool name="use_home">false</bool>
 
+    <!-- Push server url -->
+    <string name="push_server_url" translatable="false"></string>
 </resources>
 
 

+ 7 - 0
src/main/res/values/strings.xml

@@ -34,6 +34,7 @@
     <string name="drawer_item_settings">Settings</string>
     <string name="drawer_item_uploads_list">Uploads</string>
     <string name="drawer_item_activities">Activities</string>
+    <string name="drawer_item_notifications">Notifications</string>
     <string name="drawer_quota">%1$s of %2$s used</string>
 	<string name="drawer_close">Close</string>
     <string name="drawer_open">Open</string>
@@ -581,6 +582,10 @@
     <string name="activity_list_loading_activity">Loading activities&#8230;</string>
     <string name="activity_list_no_results">No activities found.</string>
 
+    <string name="notifications_loading_activity">Loading notifications&#8230;</string>
+    <string name="notifications_no_results_headline">No notifications</string>
+    <string name="notifications_no_results_message">Please check back later.</string>
+
     <string name="upload_file_dialog_title">Input upload filename and filetype</string>
     <string name="upload_file_dialog_filename">Filename</string>
     <string name="upload_file_dialog_filetype">Filetype</string>
@@ -621,5 +626,7 @@
     <string name="activities_no_results_headline">No activity yet</string>
     <string name="activities_no_results_message">This stream will show events like\nadditions, changes &amp; shares</string>
 
+    <!-- Notifications -->
+    <string name="new_notification_received">New notification received</string>
 
 </resources>

+ 100 - 0
src/modified/AndroidManifest.xml

@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Nextcloud Android client application
+
+  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 version 2,
+  as published by the Free Software Foundation.
+
+  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/>.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="com.owncloud.android"
+          android:versionCode="10040299"
+          android:versionName="1.4.2">
+
+    <application
+        android:name=".MainApp"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:fullBackupContent="@xml/backup_config"
+        android:theme="@style/Theme.ownCloud.Toolbar"
+        android:manageSpaceActivity="com.owncloud.android.ui.activity.ManageSpaceActivity">
+        <activity
+            android:name=".ui.activity.ModifiedFileDisplayActivity"
+            android:label="@string/app_name"
+            android:theme="@style/Theme.ownCloud.Toolbar.Drawer">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:name=".authentication.ModifiedAuthenticatorActivity"
+            android:exported="true"
+            android:launchMode="singleTask"
+            android:theme="@style/Theme.ownCloud.noActionBar.Login">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+
+                <data android:scheme="@string/oauth2_redirect_scheme" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="com.owncloud.android.workaround.accounts.CREATE" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="@string/login_data_own_scheme" android:host="login"/>
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:name=".ui.activity.FileDisplayActivity"
+            tools:node="remove"/>
+
+        <activity-alias
+            android:name=".ui.activity.FileDisplayActivity"
+            android:targetActivity=".ui.activity.ModifiedFileDisplayActivity"
+            tools:replace="android:targetActivity"/>
+
+        <activity-alias
+            android:name=".authentication.AuthenticatorActivity"
+            android:targetActivity=".authentication.ModifiedAuthenticatorActivity"
+            tools:replace="android:targetActivity"/>
+
+
+        <service
+            android:name=".services.firebase.NCFirebaseMessagingService">
+            <intent-filter>
+                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
+            </intent-filter>
+        </service>
+
+        <service
+            android:name=".services.firebase.NCFirebaseInstanceIDService">
+            <intent-filter>
+                <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
+            </intent-filter>
+        </service>
+
+    </application>
+
+</manifest>

+ 41 - 0
src/modified/java/com/owncloud/android/authentication/ModifiedAuthenticatorActivity.java

@@ -0,0 +1,41 @@
+package com.owncloud.android.authentication;
+
+import android.os.Bundle;
+
+import com.owncloud.android.utils.GooglePlayUtils;
+
+/**
+ * Nextcloud Android client 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 Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+public class ModifiedAuthenticatorActivity extends AuthenticatorActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        GooglePlayUtils.checkPlayServices(this);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        GooglePlayUtils.checkPlayServices(this);
+    }
+
+}

+ 45 - 0
src/modified/java/com/owncloud/android/services/firebase/NCFirebaseInstanceIDService.java

@@ -0,0 +1,45 @@
+/**
+ * Nextcloud Android client 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 Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.services.firebase;
+
+import android.text.TextUtils;
+
+import com.google.firebase.iid.FirebaseInstanceId;
+import com.google.firebase.iid.FirebaseInstanceIdService;
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.db.PreferenceManager;
+import com.owncloud.android.utils.PushUtils;
+
+public class NCFirebaseInstanceIDService extends FirebaseInstanceIdService {
+    private static final String TAG = "NCFirebaseInstanceID";
+
+    @Override
+    public void onTokenRefresh() {
+        //You can implement this method to store the token on your server
+        if (!TextUtils.isEmpty(getResources().getString(R.string.push_server_url))) {
+            PreferenceManager.setPushToken(MainApp.getAppContext(), FirebaseInstanceId.getInstance().getToken());
+            PreferenceManager.setPushTokenUpdateTime(MainApp.getAppContext(), System.currentTimeMillis());
+
+            PushUtils.pushRegistrationToServer();
+        }
+    }
+}
+

+ 66 - 0
src/modified/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java

@@ -0,0 +1,66 @@
+/**
+ * Nextcloud Android client 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 Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.services.firebase;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.support.v4.app.NotificationCompat;
+
+import com.google.firebase.messaging.FirebaseMessagingService;
+import com.google.firebase.messaging.RemoteMessage;
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.ui.activity.NotificationsActivity;
+
+public class NCFirebaseMessagingService extends FirebaseMessagingService {
+    private static final String TAG = "NCFirebaseMessaging";
+
+    @Override
+    public void onMessageReceived(RemoteMessage remoteMessage) {
+        super.onMessageReceived(remoteMessage);
+
+        sendNotification(MainApp.getAppContext().getString(R.string.new_notification_received));
+    }
+
+    private void sendNotification(String contentTitle) {
+        Intent intent = new Intent(this, NotificationsActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent,
+                PendingIntent.FLAG_ONE_SHOT);
+
+        Uri defaultSoundUri= RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
+        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
+                .setSmallIcon(R.mipmap.ic_launcher)
+                .setContentTitle(contentTitle)
+                .setSound(defaultSoundUri)
+                .setAutoCancel(true)
+                .setContentIntent(pendingIntent);
+
+        NotificationManager notificationManager =
+                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+
+        notificationManager.notify(0, notificationBuilder.build());
+    }
+
+}

+ 36 - 0
src/modified/java/com/owncloud/android/ui/activity/ModifiedFileDisplayActivity.java

@@ -0,0 +1,36 @@
+/**
+ * Nextcloud Android client 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 Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.activity;
+
+import com.owncloud.android.ui.events.TokenPushEvent;
+import com.owncloud.android.utils.PushUtils;
+
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+public class ModifiedFileDisplayActivity extends FileDisplayActivity {
+
+    @Subscribe(threadMode = ThreadMode.BACKGROUND)
+    public void onMessageEvent(TokenPushEvent event) {
+        PushUtils.pushRegistrationToServer();
+    }
+
+}

+ 49 - 0
src/modified/java/com/owncloud/android/utils/GooglePlayUtils.java

@@ -0,0 +1,49 @@
+/**
+ * Nextcloud Android client 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 Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.utils;
+
+import android.app.Activity;
+import android.util.Log;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+
+public class GooglePlayUtils {
+    private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
+    private static final String TAG = "GooglePlayUtils";
+
+    public static boolean checkPlayServices(Activity activity) {
+        GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
+        int resultCode = apiAvailability.isGooglePlayServicesAvailable(activity);
+        if (resultCode != ConnectionResult.SUCCESS) {
+            if (apiAvailability.isUserResolvableError(resultCode)) {
+                apiAvailability.getErrorDialog(activity, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST)
+                        .show();
+            } else {
+                Log.i(TAG, "This device is not supported.");
+                activity.finish();
+            }
+            return false;
+        }
+        return true;
+    }
+
+}

+ 248 - 0
src/modified/java/com/owncloud/android/utils/PushUtils.java

@@ -0,0 +1,248 @@
+/**
+ * Nextcloud Android client 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 Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.utils;
+
+import android.accounts.Account;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Base64;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.authentication.AccountUtils;
+import com.owncloud.android.db.PreferenceManager;
+import com.owncloud.android.lib.common.OwnCloudAccount;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
+import com.owncloud.android.lib.common.operations.RemoteOperation;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.lib.resources.notifications.RegisterAccountDeviceForNotificationsOperation;
+import com.owncloud.android.lib.resources.notifications.RegisterAccountDeviceForProxyOperation;
+import com.owncloud.android.lib.resources.notifications.models.PushResponse;
+
+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;
+
+public class PushUtils {
+
+    private static final String TAG = "PushUtils";
+    private static final String KEYPAIR_FOLDER = "nc-keypair";
+    private static final String KEYPAIR_FILE_NAME = "push_key";
+    private static final String KEYPAIR_PRIV_EXTENSION = ".priv";
+    private static final String KEYPAIR_PUB_EXTENSION = ".pub";
+
+    public static 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_OC.d(TAG, "SHA-512 algorithm not supported");
+        }
+        return "";
+    }
+
+    private static 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();
+    }
+
+    public static int generateRsa2048KeyPair() {
+        String keyPath = MainApp.getStoragePath() + File.separator + MainApp.getDataFolder() + File.separator
+                + KEYPAIR_FOLDER;
+
+        String privateKeyPath = keyPath + File.separator + KEYPAIR_FILE_NAME + KEYPAIR_PRIV_EXTENSION;
+        String publicKeyPath = keyPath + File.separator + KEYPAIR_FILE_NAME + KEYPAIR_PUB_EXTENSION;
+        File keyPathFile = new File(keyPath);
+
+        if (!new File(privateKeyPath).exists() && !new File(publicKeyPath).exists()) {
+            try {
+                if (!keyPathFile.exists()) {
+                    keyPathFile.mkdir();
+                }
+                KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+                keyGen.initialize(2048);
+
+                KeyPair pair = keyGen.generateKeyPair();
+                int statusPrivate = saveKeyToFile(pair.getPrivate(), privateKeyPath);
+                int statusPublic = saveKeyToFile(pair.getPublic(), publicKeyPath);
+
+                if (statusPrivate == 0 && statusPublic == 0) {
+                    // all went well
+                    return 0;
+                } else {
+                    return -2;
+                }
+            } catch (NoSuchAlgorithmException e) {
+                Log_OC.d(TAG, "RSA algorithm not supported");
+            }
+        } else {
+            // we already have the key
+            return -1;
+        }
+
+        // we failed to generate the key
+        return -2;
+    }
+
+    public static void pushRegistrationToServer() {
+        String token = PreferenceManager.getPushToken(MainApp.getAppContext());
+        if (!TextUtils.isEmpty(MainApp.getAppContext().getResources().getString(R.string.push_server_url)) &&
+                !TextUtils.isEmpty(token)) {
+            PushUtils.generateRsa2048KeyPair();
+            String pushTokenHash = PushUtils.generateSHA512Hash(token).toLowerCase();
+            PublicKey devicePublicKey = (PublicKey) PushUtils.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";
+
+                Context context = MainApp.getAppContext();
+                for (Account account : AccountUtils.getAccounts(context)) {
+                    try {
+                        OwnCloudAccount ocAccount = new OwnCloudAccount(account, context);
+                        OwnCloudClient mClient = OwnCloudClientManagerFactory.getDefaultSingleton().
+                                getClientFor(ocAccount, context);
+
+                        RemoteOperation registerAccountDeviceForNotificationsOperation =
+                                new RegisterAccountDeviceForNotificationsOperation(pushTokenHash,
+                                        publicKey,
+                                        context.getResources().getString(R.string.push_server_url));
+
+                        RemoteOperationResult remoteOperationResult = registerAccountDeviceForNotificationsOperation.
+                                execute(mClient);
+
+                        if (remoteOperationResult.isSuccess()) {
+                            PushResponse pushResponse = remoteOperationResult.getPushResponseData();
+
+                            RemoteOperation registerAccountDeviceForProxyOperation = new
+                                    RegisterAccountDeviceForProxyOperation(
+                                    context.getResources().getString(R.string.push_server_url),
+                                    token, pushResponse.getDeviceIdentifier(), pushResponse.getSignature(),
+                                    pushResponse.getPublicKey());
+
+                            remoteOperationResult = registerAccountDeviceForProxyOperation.execute(mClient);
+                            PreferenceManager.setPushTokenLastSentTime(MainApp.getAppContext(),
+                                    System.currentTimeMillis());
+
+                        }
+                    } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
+                        Log_OC.d(TAG, "Failed to find an account");
+                    } catch (AuthenticatorException e) {
+                        Log_OC.d(TAG, "Failed via AuthenticatorException");
+                    } catch (IOException e) {
+                        Log_OC.d(TAG, "Failed via IOException");
+                    } catch (OperationCanceledException e) {
+                        Log_OC.d(TAG, "Failed via OperationCanceledException");
+                    }
+                }
+            }
+        }
+    }
+
+    public static Key readKeyFromFile(boolean readPublicKey) {
+        String keyPath = MainApp.getStoragePath() + File.separator + MainApp.getDataFolder() + File.separator
+                + KEYPAIR_FOLDER;
+        ;
+        String privateKeyPath = keyPath + File.separator + KEYPAIR_FILE_NAME + KEYPAIR_PRIV_EXTENSION;
+        String publicKeyPath = keyPath + File.separator + KEYPAIR_FILE_NAME + KEYPAIR_PUB_EXTENSION;
+
+        String path;
+
+        if (readPublicKey) {
+            path = publicKeyPath;
+        } else {
+            path = privateKeyPath;
+        }
+
+        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_OC.d(TAG, "Failed to find path while reading the Key");
+        } catch (IOException e) {
+            Log_OC.d(TAG, "IOException while reading the key");
+        } catch (InvalidKeySpecException e) {
+            Log_OC.d(TAG, "InvalidKeySpecException while reading the key");
+        } catch (NoSuchAlgorithmException e) {
+            Log_OC.d(TAG, "RSA algorithm not supported");
+        }
+
+        return null;
+    }
+
+    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_OC.d(TAG, "Failed to save key to file");
+        } catch (IOException e) {
+            Log_OC.d(TAG, "Failed to save key to file via IOException");
+        }
+
+        return -1;
+    }
+}

+ 5 - 0
src/modified/res/values/setup.xml

@@ -109,6 +109,9 @@
 
     <!-- login data links -->
     <string name="login_data_own_scheme" translatable="false">cloud</string>
+    <!-- url for webview login, with the protocol prefix
+    If set, will replace all other login methods available -->
+    <string name="webview_login_url" translatable="false"></string>
 
     <!-- analytics enabled -->
     <bool name="analytics_enabled">false</bool>
@@ -116,6 +119,8 @@
     <!-- Files becomes Home -->
     <bool name="use_home">true</bool>
 
+    <!-- Push server url -->
+    <string name="push_server_url" translatable="false"></string>
 </resources>