├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── polar │ │ └── nextcloudservices │ │ ├── BasicNotificationProcessorTest.java │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── polar │ │ │ └── nextcloudservices │ │ │ ├── API │ │ │ ├── INextcloudAbstractAPI.java │ │ │ ├── NextcloudHttpAPI.java │ │ │ ├── NextcloudSSOAPI.java │ │ │ └── websocket │ │ │ │ ├── INotificationWebsocketEventListener.java │ │ │ │ └── NotificationWebsocket.java │ │ │ ├── Adapters │ │ │ └── CreditsAdapter.java │ │ │ ├── BootReceiver.java │ │ │ ├── Config.java │ │ │ ├── ContributorDetails.java │ │ │ ├── CreditsActivity.java │ │ │ ├── NSGlideModule.java │ │ │ ├── Notification │ │ │ ├── AbstractNotificationProcessor.java │ │ │ ├── NotificationBroadcastReceiver.java │ │ │ ├── NotificationBuilder.java │ │ │ ├── NotificationBuilderResult.java │ │ │ ├── NotificationConfig.java │ │ │ ├── NotificationController.java │ │ │ ├── NotificationControllerExtData.java │ │ │ ├── NotificationEvent.java │ │ │ ├── NotificationEventReceiver.java │ │ │ └── Processors │ │ │ │ ├── ActionsNotificationProcessor.java │ │ │ │ ├── OpenBrowserProcessor.java │ │ │ │ ├── basic │ │ │ │ ├── AppNameMapper.java │ │ │ │ └── BasicNotificationProcessor.java │ │ │ │ └── spreed │ │ │ │ ├── NextcloudTalkProcessor.java │ │ │ │ └── chat │ │ │ │ ├── Chat.java │ │ │ │ ├── ChatController.java │ │ │ │ └── ChatMessage.java │ │ │ ├── Services │ │ │ ├── ConnectionController.java │ │ │ ├── IConnectionStatusListener.java │ │ │ ├── INotificationListener.java │ │ │ ├── INotificationService.java │ │ │ ├── NotificationPollService.java │ │ │ ├── NotificationServiceBinder.java │ │ │ ├── NotificationServiceComponents.java │ │ │ ├── NotificationServiceConfig.java │ │ │ ├── NotificationServiceController.java │ │ │ ├── NotificationWebsocketService.java │ │ │ ├── Settings │ │ │ │ ├── ServiceSettingConfig.java │ │ │ │ └── ServiceSettings.java │ │ │ └── Status │ │ │ │ ├── Status.java │ │ │ │ ├── StatusCheckable.java │ │ │ │ └── StatusController.java │ │ │ ├── SettingsActivity.java │ │ │ └── Utils │ │ │ └── CommonUtil.java │ └── res │ │ ├── drawable-v24 │ │ ├── ic_launcher_foreground.xml │ │ └── user.png │ │ ├── drawable │ │ ├── ic_deck.xml │ │ ├── ic_icon_foreground.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_logo.xml │ │ └── ic_reply_icon.xml │ │ ├── layout │ │ ├── activity_credits.xml │ │ ├── credits_contributer.xml │ │ └── settings_activity.xml │ │ ├── menu │ │ └── menu_o_s_s_licenses.xml │ │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── values-night │ │ ├── booleans.xml │ │ └── colors.xml │ │ ├── values │ │ ├── arrays.xml │ │ ├── booleans.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ └── root_preferences.xml │ └── test │ └── java │ └── com │ └── polar │ └── nextcloudservices │ ├── CommonUtilTest.java │ └── ExampleUnitTest.java ├── build.gradle ├── fastlane └── metadata │ └── android │ └── en-US │ ├── changelogs │ ├── 10.txt │ ├── 11.txt │ ├── 12.txt │ ├── 13.txt │ ├── 14.txt │ ├── 15.txt │ ├── 16.txt │ ├── 17.txt │ ├── 18.txt │ ├── 19.txt │ ├── 20.txt │ ├── 4.txt │ ├── 7.txt │ ├── 8.txt │ └── 9.txt │ ├── full_description.txt │ ├── images │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ └── 5.png │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img ├── Screenshot_scaled.png └── app_icon.png └── settings.gradle /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up JDK 17 16 | uses: actions/setup-java@v1 17 | with: 18 | java-version: 17 19 | - name: Grant execute permission for gradlew 20 | run: chmod +x gradlew 21 | - name: Build with Gradle 22 | run: ./gradlew build 23 | - name: Test with Gradle 24 | run: ./gradlew test 25 | - name: Upload debug APK 26 | uses: actions/upload-artifact@v3 27 | with: 28 | name: app-debug.apk 29 | path: app/build/outputs/apk/debug/app-debug.apk 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .keys 2 | .idea 3 | *.iml 4 | .gradle 5 | /local.properties 6 | /.idea/caches 7 | /.idea/libraries 8 | /.idea/modules.xml 9 | /.idea/workspace.xml 10 | /.idea/navEditor.xml 11 | /.idea/assetWizardSettings.xml 12 | .DS_Store 13 | /build 14 | /captures 15 | .externalNativeBuild 16 | .cxx 17 | *.apk 18 | local.properties 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nextcloud services 2 | [![Java CI with Gradle](https://github.com/Andrewerr/NextcloudServices/actions/workflows/gradle.yml/badge.svg)](https://github.com/Andrewerr/NextcloudServices/actions/workflows/gradle.yml) 3 | [![F-Droid build](https://img.shields.io/f-droid/v/com.polar.nextcloudservices.svg?logo=f-droid)](https://f-droid.org/wiki/page/com.polar.nextcloudservices/lastbuild) 4 | ![Github tag](https://img.shields.io/github/v/tag/Andrewerr/NextcloudServices?logo=github) 5 |
6 | [](https://f-droid.org/en/packages/com.polar.nextcloudservices/) 7 |
8 | Nextcloud services is a simple app to poll notifications from your Nextcloud server without using proprietary Google Play services. 9 | ## Screenshots 10 | ![Screenshot 1](https://github.com/Andrewerr/NextcloudServices/raw/main/img/Screenshot_scaled.png) 11 | ## Instructions 12 | WITHOUT NEXTCLOUD APP: 13 | * At your Nextcloud open settings and navigate to "Security" 14 | * Generate per-app password 15 | * Enter you login and server address into the app(Enter server address without `https://` prefix) 16 | * Enter generated per-app password 17 | * On Nextcloud server click "Add" button to add generated password to list of authenticated devices(Additionally it is recommended to disable file access for this per-app password) 18 | 19 | IMPORTANT: Do **NOT** ommit first two steps - this may be risky for your security 20 | 21 | WITH NEXTCLOUD APP: 22 | * Click "Log-in via Nextcloud app" 23 | * Select account you want to use 24 | * In next dialog click "Allow button" 25 | 26 | ## Getting bleeding-edge version 27 | If you would like to test new features and fixes as they are developed you may download a bleeding-edge build from [Github Actions](https://github.com/Andrewerr/NextcloudServices/actions). Here is the [instruction](https://docs.github.com/en/actions/managing-workflow-runs/downloading-workflow-artifacts) of how you can do it. Also please note that builds done by Actions are *not* signed, so you would need to delete your app installes from F-Droid(if you have installed it) and use `adb` to install app. 28 | 29 | ## Donate 30 | If you like this app please donate:
31 | [![LiberaPay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/Andrewerr/donate) 32 | 33 | 34 | ## Credits 35 | * [Deck Android app](https://github.com/stefan-niedermann/nextcloud-deck) for deck logo 36 | * [Nextcloud app](https://github.com/nextcloud/android/) for Nextcloud logo and spreed(talk) logo 37 | * [@penguin86](https://github.com/penguin86) for fixing bugs and suggesting new ideas 38 | * [@Donnnno](https://github.com/Donnnno) for creating app icon 39 | * [@invissvenska](https://github.com/invissvenska) for [NumberPickerPreference](https://github.com/invissvenska/NumberPickerPreference/) (licensed under LGPL-3.0) 40 | * [@Devansh-Gaur-1611](https://github.com/Devansh-Gaur-1611) for creating credits activity in the app 41 | * [@freeflyk](https://github.com/freeflyk) for improvements, fixes and adding new features 42 | * [@stefan-niedermann](https://github.com/stefan-niedermann) for redesigning app to Material You design and creating monochrome icon 43 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdkVersion 34 7 | 8 | defaultConfig { 9 | applicationId "com.polar.nextcloudservices" 10 | minSdkVersion 25 11 | targetSdkVersion 34 12 | versionCode 20 13 | versionName '1.1-beta20' 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | namespace 'com.polar.nextcloudservices' 29 | lint { 30 | abortOnError false 31 | } 32 | } 33 | 34 | dependencies { 35 | 36 | implementation 'androidx.appcompat:appcompat:1.6.1' 37 | implementation 'com.google.android.material:material:1.11.0' 38 | implementation 'androidx.preference:preference:1.2.0' 39 | implementation 'androidx.core:core:1.12.0' 40 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 41 | implementation 'androidx.browser:browser:1.5.0' 42 | testImplementation 'junit:junit:4.+' 43 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 44 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 45 | implementation 'com.github.invissvenska:NumberPickerPreference:1.0.3' 46 | implementation "com.github.nextcloud:Android-SingleSignOn:0.6.1" 47 | implementation 'org.java-websocket:Java-WebSocket:1.5.4' 48 | 49 | // Image Loading Library:Glide 50 | implementation 'com.github.bumptech.glide:glide:4.16.0' 51 | annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1' 52 | } 53 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/polar/nextcloudservices/BasicNotificationProcessorTest.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotNull; 5 | 6 | import android.content.Context; 7 | 8 | import androidx.test.ext.junit.runners.AndroidJUnit4; 9 | import androidx.test.platform.app.InstrumentationRegistry; 10 | 11 | import com.polar.nextcloudservices.Notification.Processors.basic.AppNameMapper; 12 | 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | 16 | @RunWith(AndroidJUnit4.class) 17 | public class BasicNotificationProcessorTest { 18 | /** 19 | * Checks that mapper for apps works maps all mappable apps 20 | */ 21 | @Test 22 | public void testAppNameMapperStaticMap() { 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | for(String appName : AppNameMapper.MAPPABLE_APPS){ 25 | String result = AppNameMapper.getPrettifiedAppNameFromMapping(appContext, appName); 26 | assertNotNull(result); 27 | assertEquals(result, AppNameMapper.getPrettifiedAppName(appContext, appName)); 28 | } 29 | } 30 | 31 | /** 32 | * Checks that mapper prettifies through opportunistic format guessing 33 | */ 34 | @Test 35 | public void testAppNameMapperNonStatic(){ 36 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 37 | String name = "my_app_name"; 38 | assertEquals("My app name ", AppNameMapper.getPrettifiedAppName(appContext, name)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/polar/nextcloudservices/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.polar.nextcloudservices", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/API/INextcloudAbstractAPI.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.API; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | import com.polar.nextcloudservices.API.websocket.NotificationWebsocket; 6 | import com.polar.nextcloudservices.API.websocket.INotificationWebsocketEventListener; 7 | import com.polar.nextcloudservices.Services.INotificationListener; 8 | import com.polar.nextcloudservices.Services.Status.StatusCheckable; 9 | 10 | import org.json.JSONException; 11 | import org.json.JSONObject; 12 | 13 | import java.io.IOException; 14 | 15 | /* 16 | * Nextcloud abstract API creates possibility to use different libraries for 17 | * polling for notifications. This is needed to use Nextcloud SSO library 18 | * since it does not give per-app key. 19 | * The inheritors of this interface should be passed to NotificationService. 20 | */ 21 | public interface INextcloudAbstractAPI extends StatusCheckable { 22 | /** 23 | * Gets all notifications from server 24 | * @param service PollUpdateListener which handles notifications 25 | * @return notifications response as a JSONObject 26 | */ 27 | JSONObject getNotifications(INotificationListener service); 28 | 29 | /** 30 | * Removes notification from server 31 | * @param id id of notification to remove 32 | */ 33 | void removeNotification(int id); 34 | 35 | /** 36 | * Sends reply to talk chatroom 37 | * @param chatroom id of a chat 38 | * @param message message to send 39 | * @throws IOException in case of network error 40 | * @throws JSONException in case of low-level errors(like allocation failure), 41 | * should be almost impossible to get it here as JSON should be used for serialization only 42 | */ 43 | void sendTalkReply(String chatroom, String message) throws IOException, JSONException; 44 | 45 | /** 46 | * Get user avatar 47 | * @param userId username to get avatar of 48 | * @return avatar bitmap 49 | * @throws Exception in case of any errors 50 | */ 51 | Bitmap getUserAvatar(String userId) throws Exception; 52 | 53 | /** 54 | * Gets image preview from server 55 | * @param path path to image 56 | * @return bitmap received from server 57 | * @throws Exception in case of any errors 58 | */ 59 | Bitmap getImagePreview(String path) throws Exception; 60 | 61 | /** 62 | * Executes action which is inside of notifications 63 | * @param link Link to action 64 | * @param method method which should be used for querying link 65 | * @throws Exception in case of any errors 66 | */ 67 | void sendAction(String link, String method) throws Exception; 68 | 69 | /** 70 | * @doc Checks new notifications without querying all of them directly 71 | * @return true if there is new notifications on server 72 | * @throws Exception in case of any error 73 | */ 74 | boolean checkNewNotifications() throws Exception; 75 | 76 | /** 77 | * @return WebsocketClient instance which holds pre-authorized connection 78 | * @throws Exception in case of any unhandlable error 79 | * @doc Gets websocket client which is authorized and receives notification updates 80 | */ 81 | NotificationWebsocket getNotificationsWebsocket(INotificationWebsocketEventListener listener) throws Exception; 82 | 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/API/NextcloudHttpAPI.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.API; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.util.Base64; 7 | import android.util.Log; 8 | 9 | import androidx.annotation.NonNull; 10 | 11 | import com.polar.nextcloudservices.API.websocket.NotificationWebsocket; 12 | import com.polar.nextcloudservices.API.websocket.INotificationWebsocketEventListener; 13 | import com.polar.nextcloudservices.BuildConfig; 14 | import com.polar.nextcloudservices.Services.INotificationListener; 15 | import com.polar.nextcloudservices.Services.Settings.ServiceSettings; 16 | import com.polar.nextcloudservices.Services.Status.Status; 17 | 18 | import org.json.JSONException; 19 | import org.json.JSONObject; 20 | 21 | import java.io.BufferedReader; 22 | import java.io.IOException; 23 | import java.io.InputStreamReader; 24 | import java.io.OutputStream; 25 | import java.net.HttpURLConnection; 26 | import java.net.URI; 27 | import java.net.URL; 28 | import java.nio.charset.StandardCharsets; 29 | import java.util.Objects; 30 | 31 | import javax.net.ssl.HttpsURLConnection; 32 | 33 | public class NextcloudHttpAPI implements INextcloudAbstractAPI { 34 | private final String TAG = "NextcloudHttpAPI"; 35 | private final String UA = "NextcloudServices/" + BuildConfig.VERSION_NAME; 36 | private String mStatusString = "Updating settings"; 37 | private boolean lastPollSuccessful = false; 38 | private final ServiceSettings mServiceSettings; 39 | private String mETag = ""; 40 | 41 | public NextcloudHttpAPI(ServiceSettings settings){ 42 | mServiceSettings = settings; 43 | } 44 | private static String getAuth(String user, String password) { 45 | //Log.d("NotificationService.PollTask",user+":"+password); 46 | return Base64.encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8), Base64.DEFAULT); 47 | } 48 | 49 | private HttpURLConnection request(String path, String method, 50 | Boolean setAccept) throws IOException { 51 | String user = mServiceSettings.getUsername(); 52 | String password = mServiceSettings.getPassword(); 53 | String baseUrl = mServiceSettings.getServer(); 54 | String prefix = "https://"; 55 | if (mServiceSettings.getUseHttp()) { 56 | prefix = "http://"; 57 | } 58 | String endpoint = prefix + baseUrl + path; 59 | //Log.d(TAG, endpoint); 60 | URL url = new URL(endpoint); 61 | HttpURLConnection conn; 62 | if (mServiceSettings.getUseHttp()) { 63 | conn = (HttpURLConnection) url.openConnection(); 64 | } else { 65 | conn = (HttpsURLConnection) url.openConnection(); 66 | } 67 | conn.setRequestProperty("Authorization", "Basic " + getAuth(user, password)); 68 | conn.setRequestProperty("Host", url.getHost()); 69 | conn.setRequestProperty("User-agent", UA); 70 | conn.setRequestProperty("OCS-APIRequest", "true"); 71 | if (setAccept) { 72 | conn.setRequestProperty("Accept", "application/json"); 73 | } 74 | conn.setRequestMethod(method); 75 | conn.setReadTimeout(60000); 76 | conn.setConnectTimeout(5000); 77 | return conn; 78 | } 79 | 80 | @Override 81 | public void removeNotification(int id) { 82 | Log.d(TAG, "Removing notification " + id); 83 | try { 84 | String prefix = "https://"; 85 | if (mServiceSettings.getUseHttp()) { 86 | prefix = "http://"; 87 | } 88 | HttpURLConnection conn = 89 | request("/ocs/v2.php/apps/notifications/api/v2/notifications/" + id, 90 | "DELETE", false); 91 | String responseCode = Integer.toString(conn.getResponseCode()); 92 | Log.d(TAG, "--> DELETE " + prefix + mServiceSettings.getServer() 93 | + "/ocs/v2.php/apps/notifications/api/v2/notifications/" + 94 | id + " -- " + responseCode); 95 | } catch (IOException e) { 96 | Log.e(TAG, "Failed to DELETE notification: " + e.getLocalizedMessage()); 97 | Log.d(TAG, "Exception was: " + e); 98 | } 99 | } 100 | 101 | private static String getEndpoint(@NonNull ServiceSettings settings){ 102 | String baseUrl = settings.getServer(); 103 | String prefix = "https://"; 104 | if (settings.getUseHttp()) { 105 | prefix = "http://"; 106 | } 107 | return prefix + baseUrl; 108 | } 109 | 110 | private HttpURLConnection getBaseConnection(URL url, String method) 111 | throws IOException { 112 | HttpURLConnection conn; 113 | if (mServiceSettings.getUseHttp()) { 114 | conn = (HttpURLConnection) url.openConnection(); 115 | } else { 116 | conn = (HttpsURLConnection) url.openConnection(); 117 | } 118 | conn.setRequestProperty("Authorization", "Basic " + getAuth(mServiceSettings.getUsername(), 119 | mServiceSettings.getPassword())); 120 | conn.setRequestProperty("Host", url.getHost()); 121 | conn.setRequestProperty("User-agent", UA); 122 | conn.setRequestProperty("OCS-APIRequest", "true"); 123 | conn.setRequestProperty("Accept", "application/json"); 124 | conn.setRequestProperty("Content-Type", "application/json"); 125 | conn.setRequestMethod(method); 126 | return conn; 127 | } 128 | 129 | @Override 130 | public void sendTalkReply(String chatroom, String message) throws IOException { 131 | String endpoint = getEndpoint(mServiceSettings) + "/ocs/v2.php/apps/spreed/api/v1/chat/" + chatroom; 132 | URL url = new URL(endpoint); 133 | HttpURLConnection conn = getBaseConnection(url, "POST"); 134 | conn.setDoOutput(true); 135 | conn.setConnectTimeout(5000); 136 | 137 | //FIXME: create separate params generator 138 | final String params = "{\"message\": \"" + message + "\", \"chatroom\": \"" + chatroom + "\"}"; 139 | OutputStream os = conn.getOutputStream(); 140 | os.write(params.getBytes(StandardCharsets.UTF_8)); 141 | os.flush(); 142 | os.close(); 143 | 144 | int code = conn.getResponseCode(); 145 | Log.d(TAG, "--> POST " + endpoint + " -- " + code); 146 | 147 | } 148 | 149 | public Bitmap getUserAvatar(String userId) throws IOException { 150 | HttpURLConnection connection = request("/index.php/avatar/"+userId+"/256", 151 | "GET", false); 152 | connection.setDoInput(true); 153 | return BitmapFactory.decodeStream(connection.getInputStream()); 154 | } 155 | 156 | @Override 157 | public Bitmap getImagePreview(String imageId) throws Exception { 158 | HttpURLConnection connection = request( "/index.php/core/preview?fileId=" 159 | + imageId + "&x=100&y=100&a=1", 160 | "GET", false); 161 | connection.setRequestProperty("Accept", "image/*"); 162 | connection.setDoInput(true); 163 | 164 | int responseCode = connection.getResponseCode(); 165 | Log.d(TAG, "--> GET " + getEndpoint(mServiceSettings) 166 | + "/index.php/core/preview?fileId="+imageId+"&x=100&y=100&a=1 -- " + responseCode); 167 | 168 | return BitmapFactory.decodeStream(connection.getInputStream()); 169 | } 170 | 171 | @Override 172 | public void sendAction(String link, 173 | String method) throws Exception { 174 | String endpoint = getEndpoint(mServiceSettings) + link; 175 | URL url = new URL(endpoint); 176 | HttpURLConnection connection = getBaseConnection(url, method); 177 | connection.setConnectTimeout(5000); 178 | connection.setDoInput(true); 179 | 180 | int responseCode = connection.getResponseCode(); 181 | Log.d(TAG, "--> " + method + getEndpoint(mServiceSettings) 182 | + link + "--" + responseCode); 183 | } 184 | 185 | @Override 186 | public boolean checkNewNotifications() throws Exception { 187 | HttpURLConnection connection = request( 188 | "/ocs/v2.php/apps/notifications/api/v2/notifications", 189 | "HEAD", false); 190 | connection.setConnectTimeout(5000); 191 | connection.setDoInput(true); 192 | String lastETag = connection.getHeaderField("ETag"); 193 | if(!Objects.equals(lastETag, mETag)){ 194 | Log.d(TAG, "Detected new notifications"); 195 | mETag = lastETag; 196 | return true; 197 | } 198 | return false; 199 | } 200 | 201 | @NonNull 202 | private JSONObject readConnectionToJson(@NonNull HttpURLConnection conn) 203 | throws IOException, JSONException{ 204 | BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); 205 | StringBuilder buffer = new StringBuilder(); 206 | String line; 207 | while ((line = in.readLine()) != null) { 208 | buffer.append(line); 209 | } 210 | in.close(); 211 | return new JSONObject(buffer.toString()); 212 | } 213 | 214 | //See get_endpoint from 215 | // https://github.com/nextcloud/notify_push/blob/main/test_client/src/main.rs 216 | @NonNull 217 | private String getWebsocketURL() throws Exception { 218 | HttpURLConnection connection = request( 219 | "/ocs/v2.php/cloud/capabilities", 220 | "GET", true); 221 | connection.setConnectTimeout(5000); 222 | connection.setDoInput(true); 223 | JSONObject capabilities = readConnectionToJson(connection); 224 | return capabilities.getJSONObject("ocs") 225 | .getJSONObject("data") 226 | .getJSONObject("capabilities") 227 | .getJSONObject("notify_push") 228 | .getJSONObject("endpoints") 229 | .getString("websocket"); 230 | } 231 | 232 | @Override 233 | public NotificationWebsocket getNotificationsWebsocket(INotificationWebsocketEventListener listener) throws Exception { 234 | Log.i(TAG, "Starting new websocket connection"); 235 | String endpoint = ""; 236 | try { 237 | endpoint = getWebsocketURL(); 238 | } catch (Exception e){ 239 | Log.e(TAG, "Can not get websocket URL", e); 240 | return null; 241 | } 242 | NotificationWebsocket client = new NotificationWebsocket(new URI(endpoint), 243 | mServiceSettings.getUsername(), 244 | mServiceSettings.getPassword(), 245 | listener, this); 246 | client.connect(); 247 | return client; 248 | } 249 | 250 | @Override 251 | public JSONObject getNotifications(INotificationListener service) { 252 | try { 253 | HttpURLConnection conn = request("/ocs/v2.php/apps/notifications/api/v2/notifications", 254 | "GET", true); 255 | conn.setDoInput(true); 256 | 257 | String responseCode = Integer.toString(conn.getResponseCode()); 258 | Log.d(TAG, "--> GET "+ getEndpoint(mServiceSettings) + "/ocs/v2.php/apps/notifications/api/v2/notifications -- " + responseCode); 259 | 260 | //Log.d(TAG, buffer.toString()); 261 | JSONObject response = readConnectionToJson(conn); 262 | lastPollSuccessful = true; 263 | 264 | service.onNewNotifications(response); 265 | return response; 266 | } catch (JSONException e) { 267 | Log.e(TAG, "Error parsing JSON"); 268 | e.printStackTrace(); 269 | mStatusString = "Disconnected: server has sent bad response: " + e.getLocalizedMessage(); 270 | return null; 271 | } catch (java.io.FileNotFoundException e) { 272 | e.printStackTrace(); 273 | mStatusString = "Disconnected: File not found: check your credentials and Nextcloud instance."; 274 | return null; 275 | } catch (IOException e) { 276 | Log.e(TAG, "Error while getting response"); 277 | e.printStackTrace(); 278 | mStatusString = "Disconnected: I/O error: " + e.getLocalizedMessage(); 279 | return null; 280 | } catch (Exception e) { 281 | e.printStackTrace(); 282 | mStatusString = "Disconnected: " + e.getLocalizedMessage(); 283 | return null; 284 | } 285 | } 286 | 287 | @Override 288 | public Status getStatus(Context context) { 289 | if(lastPollSuccessful){ 290 | return Status.Ok(); 291 | } 292 | return Status.Failed(mStatusString); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/API/NextcloudSSOAPI.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.API; 2 | 3 | /* 4 | * Implements API for accounts imported from nextcloud. 5 | */ 6 | 7 | import android.content.Context; 8 | import android.graphics.Bitmap; 9 | import android.graphics.BitmapFactory; 10 | import android.net.Uri; 11 | import android.util.Log; 12 | 13 | import com.google.gson.GsonBuilder; 14 | import com.nextcloud.android.sso.QueryParam; 15 | import com.nextcloud.android.sso.aidl.NextcloudRequest; 16 | import com.nextcloud.android.sso.api.NextcloudAPI; 17 | import com.nextcloud.android.sso.model.SingleSignOnAccount; 18 | import com.polar.nextcloudservices.API.websocket.NotificationWebsocket; 19 | import com.polar.nextcloudservices.API.websocket.INotificationWebsocketEventListener; 20 | import com.polar.nextcloudservices.Services.INotificationListener; 21 | import com.polar.nextcloudservices.Services.Status.Status; 22 | 23 | import org.json.JSONException; 24 | import org.json.JSONObject; 25 | 26 | import java.io.BufferedReader; 27 | import java.io.InputStream; 28 | import java.io.InputStreamReader; 29 | import java.util.Collection; 30 | import java.util.HashMap; 31 | import java.util.LinkedList; 32 | import java.util.List; 33 | import java.util.Map; 34 | 35 | import kotlin.NotImplementedError; 36 | 37 | public class NextcloudSSOAPI implements INextcloudAbstractAPI { 38 | final private NextcloudAPI API; 39 | final private static String TAG = "NextcloudSSOAPI"; 40 | private boolean lastPollSuccessful = false; 41 | private String mStatusString = "Updating settings"; 42 | private String mETag = ""; 43 | 44 | public NextcloudSSOAPI(Context context, SingleSignOnAccount ssoAccount) { 45 | NextcloudAPI.ApiConnectedListener apiCallback = new NextcloudAPI.ApiConnectedListener() { 46 | @Override 47 | public void onConnected() { 48 | /*stub*/ 49 | } 50 | 51 | @Override 52 | public void onError(Exception ex) { 53 | Log.e(TAG, "Exception in Nextcloud API"); 54 | ex.printStackTrace(); 55 | } 56 | }; 57 | API = new NextcloudAPI(context, ssoAccount, new GsonBuilder().create(), apiCallback); 58 | } 59 | 60 | @Override 61 | public JSONObject getNotifications(INotificationListener service) { 62 | Log.d(TAG, "getNotifications"); 63 | Map> header = new HashMap<>(); 64 | LinkedList values = new LinkedList<>(); 65 | values.add("application/json"); 66 | header.put("Accept", values); 67 | 68 | NextcloudRequest request = new NextcloudRequest.Builder().setMethod("GET") 69 | .setUrl(Uri.encode("/ocs/v2.php/apps/notifications/api/v2/notifications", "/")) 70 | .setHeader(header) 71 | .build(); 72 | StringBuilder buffer = new StringBuilder(); 73 | try { 74 | BufferedReader in = new BufferedReader( 75 | new InputStreamReader(API.performNetworkRequestV2(request).getBody())); 76 | String line; 77 | while ((line = in.readLine()) != null) { 78 | buffer.append(line); 79 | } 80 | in.close(); 81 | } catch (Exception e) { 82 | mStatusString = "Disconnected: " + e.getLocalizedMessage(); 83 | lastPollSuccessful = false; 84 | e.printStackTrace(); 85 | return null; 86 | } 87 | 88 | try { 89 | JSONObject response = new JSONObject(buffer.toString()); 90 | service.onNewNotifications(response); 91 | Log.d(TAG, "Setting lastPollSuccessful as true"); 92 | lastPollSuccessful = true; 93 | return response; 94 | } catch (JSONException e) { 95 | Log.e(TAG, "Error parsing JSON"); 96 | e.printStackTrace(); 97 | mStatusString = "Disconnected: server has sent bad response: " + e.getLocalizedMessage(); 98 | return null; 99 | } 100 | } 101 | 102 | @Override 103 | public void removeNotification(int id) { 104 | Map> header = new HashMap<>(); 105 | LinkedList values = new LinkedList<>(); 106 | values.add("application/json"); 107 | header.put("Accept", values); 108 | 109 | NextcloudRequest request = new NextcloudRequest.Builder().setMethod("DELETE") 110 | .setUrl(Uri.encode("/ocs/v2.php/apps/notifications/api/v2/notifications/"+id, "/")) 111 | .setHeader(header) 112 | .build(); 113 | try { 114 | API.performNetworkRequest(request); 115 | } catch (Exception e) { 116 | e.printStackTrace(); 117 | } 118 | } 119 | 120 | @Override 121 | public void sendTalkReply(String chatroom, String message) throws JSONException { 122 | Map> header = new HashMap<>(); 123 | LinkedList values = new LinkedList<>(); 124 | values.add("application/json"); 125 | header.put("Accept", values); 126 | header.put("Content-Type", values); 127 | 128 | JSONObject jsonParams = new JSONObject(); 129 | jsonParams.put("message", message); 130 | jsonParams.put("chatroom", chatroom); 131 | final String params = jsonParams.toString(); 132 | 133 | Log.d(TAG, "POST to /ocs/v2.php/apps/spreed/api/v1/chat/" + chatroom); 134 | 135 | NextcloudRequest request = new NextcloudRequest.Builder().setMethod("POST") 136 | .setUrl(Uri.encode("/ocs/v2.php/apps/spreed/api/v1/chat/" + chatroom, "/")) 137 | .setHeader(header) 138 | .setRequestBody(params) 139 | .build(); 140 | 141 | try { 142 | API.performNetworkRequestV2(request); 143 | } catch (Exception e) { 144 | e.printStackTrace(); 145 | } 146 | } 147 | 148 | @Override 149 | public Bitmap getUserAvatar(String userId) throws Exception { 150 | NextcloudRequest request = new NextcloudRequest.Builder().setMethod("GET") 151 | .setUrl(Uri.encode("/index.php/avatar/"+userId+"/256 ", "/")) 152 | .build(); 153 | InputStream stream = API.performNetworkRequest(request); 154 | return BitmapFactory.decodeStream(stream); 155 | } 156 | 157 | @Override 158 | public Bitmap getImagePreview(String imageId) throws Exception { 159 | Collection parameter = new LinkedList<>(); 160 | parameter.add(new QueryParam("fileId", imageId)); 161 | parameter.add(new QueryParam("x", "100")); 162 | parameter.add(new QueryParam("y", "100")); 163 | parameter.add(new QueryParam("a", "1")); 164 | NextcloudRequest request = new NextcloudRequest.Builder().setMethod("GET") 165 | .setUrl(Uri.encode("/index.php/core/preview", "/")) 166 | .setParameter(parameter) 167 | .build(); 168 | InputStream stream = API.performNetworkRequest(request); 169 | return BitmapFactory.decodeStream(stream); 170 | } 171 | 172 | @Override 173 | public void sendAction(String link, String method) throws Exception { 174 | NextcloudRequest request = new NextcloudRequest.Builder().setMethod(method) 175 | .setUrl(Uri.encode(link, "/")).build(); 176 | API.performNetworkRequest(request); 177 | } 178 | 179 | @Override 180 | public boolean checkNewNotifications() throws Exception { 181 | return true; 182 | } 183 | 184 | @Override 185 | public NotificationWebsocket getNotificationsWebsocket(INotificationWebsocketEventListener listener) throws Exception { 186 | throw new NotImplementedError("getNotificationsWebsoket() is not implemented for SSO API"); 187 | } 188 | 189 | @Override 190 | public Status getStatus(Context context) { 191 | if(lastPollSuccessful){ 192 | Log.d(TAG, "Last poll is successful"); 193 | return Status.Ok(); 194 | } 195 | return Status.Failed(mStatusString); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/API/websocket/INotificationWebsocketEventListener.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.API.websocket; 2 | 3 | import com.polar.nextcloudservices.Services.INotificationListener; 4 | 5 | public interface INotificationWebsocketEventListener extends INotificationListener { 6 | /** 7 | * Called whenever websocket is disconnected 8 | * @param isError whether disconnect resulted from error 9 | */ 10 | void onWebsocketDisconnected(boolean isError); 11 | 12 | /** 13 | * Called whenever websocket connection is established 14 | */ 15 | void onWebsocketConnected(); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/API/websocket/NotificationWebsocket.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.API.websocket; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import com.polar.nextcloudservices.API.INextcloudAbstractAPI; 7 | import com.polar.nextcloudservices.Services.Status.Status; 8 | import com.polar.nextcloudservices.Services.Status.StatusCheckable; 9 | 10 | import org.java_websocket.client.WebSocketClient; 11 | import org.java_websocket.handshake.ServerHandshake; 12 | 13 | import java.net.URI; 14 | 15 | class NotificationWebsocketConfig{ 16 | public final static String NOTIFICATION_MESSAGE = "notify_notification"; 17 | public final static String LISTEN_TOPIC ="listen notify_file_id"; 18 | } 19 | 20 | public class NotificationWebsocket extends WebSocketClient implements StatusCheckable { 21 | private final static String TAG = "NotificationWebsocket"; 22 | private final String mUsername; 23 | private final String mPassword; 24 | private final INotificationWebsocketEventListener mNotificationListener; 25 | private String mStatus; 26 | private final INextcloudAbstractAPI mAPI; 27 | private boolean isConnected; 28 | 29 | public NotificationWebsocket(URI serverUri, String username, String password, 30 | INotificationWebsocketEventListener notificationListener, 31 | INextcloudAbstractAPI api) { 32 | super(serverUri); 33 | mUsername = username; 34 | mPassword = password; 35 | mNotificationListener = notificationListener; 36 | isConnected = false; 37 | mStatus = "Disconnected"; 38 | mAPI = api; 39 | } 40 | 41 | /** 42 | * @param handshakedata The handshake of the websocket instance 43 | */ 44 | @Override 45 | public void onOpen(ServerHandshake handshakedata) { 46 | Log.i(TAG, "Connected to websocket"); 47 | send(mUsername); 48 | send(mPassword); 49 | send(NotificationWebsocketConfig.LISTEN_TOPIC); 50 | isConnected = true; 51 | mStatus = "Connected"; 52 | mNotificationListener.onWebsocketConnected(); 53 | } 54 | 55 | /** 56 | * @param message The UTF-8 decoded message that was received. 57 | */ 58 | @Override 59 | public void onMessage(String message) { 60 | Log.d(TAG, "Got message: " + message); 61 | if(message.equals(NotificationWebsocketConfig.NOTIFICATION_MESSAGE)) { 62 | mAPI.getNotifications(mNotificationListener); 63 | } 64 | } 65 | 66 | /** 67 | * @param code The codes can be looked up here: {@link CloseFrame} 68 | * @param reason Additional information string 69 | * @param remote Returns whether or not the closing of the connection was initiated by the remote 70 | * host. 71 | */ 72 | @Override 73 | public void onClose(int code, String reason, boolean remote) { 74 | if(remote){ 75 | Log.w(TAG, "Remote has disconnected, code=" + code + ", reason=" + reason); 76 | mStatus = "Remote disconnected"; 77 | } else { 78 | Log.i(TAG, "We have disconnected, code=" + code + ", reason=" + reason); 79 | mStatus = "Disconnected"; 80 | } 81 | isConnected = false; 82 | mNotificationListener.onWebsocketDisconnected(false); 83 | } 84 | 85 | /** 86 | * @param ex The exception causing this error 87 | */ 88 | @Override 89 | public void onError(Exception ex) { 90 | Log.e(TAG, "Error in websocket", ex); 91 | isConnected = false; 92 | mStatus = "Unexpected error in websocket connection"; 93 | mNotificationListener.onWebsocketDisconnected(true); 94 | } 95 | 96 | /** 97 | * @param context context which may be used for obtaining app and device status 98 | * @return status information in form of Status class 99 | */ 100 | @Override 101 | public Status getStatus(Context context) { 102 | if(isConnected){ 103 | return Status.Ok(); 104 | }else{ 105 | return Status.Failed(mStatus); 106 | } 107 | } 108 | 109 | /** 110 | * Checks that websocket is connected 111 | * @return true if websocket is connected 112 | */ 113 | public boolean getConnected(){ 114 | return isConnected; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Adapters/CreditsAdapter.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Adapters; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ArrayAdapter; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | 14 | import com.bumptech.glide.Glide; 15 | import com.bumptech.glide.annotation.GlideModule; 16 | import com.polar.nextcloudservices.R; 17 | import com.polar.nextcloudservices.ContributorDetails; 18 | 19 | import java.util.List; 20 | 21 | public class CreditsAdapter extends ArrayAdapter { 22 | Context context; 23 | List details; 24 | 25 | public CreditsAdapter(@NonNull Context context, int resource, @NonNull List objects) { 26 | super(context, resource, objects); 27 | this.context = context; 28 | this.details = objects; 29 | } 30 | 31 | @Nullable 32 | @Override 33 | public ContributorDetails getItem(int position) { 34 | return details.get(position); 35 | } 36 | 37 | @NonNull 38 | @Override 39 | public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { 40 | convertView = LayoutInflater.from(context).inflate(R.layout.credits_contributer, parent, false); 41 | ImageView UserImage = convertView.findViewById(R.id.userImage); 42 | TextView userName = convertView.findViewById(R.id.userName); 43 | TextView contribution = convertView.findViewById(R.id.contribution); 44 | TextView githubName = convertView.findViewById(R.id.github_name); 45 | userName.setText(details.get(position).Name); 46 | contribution.setText(details.get(position).contribution); 47 | githubName.setText(details.get(position).github_name); 48 | Glide.with(context).load(details.get(position).imageUrl).placeholder(R.drawable.user).circleCrop().into(UserImage); 49 | return convertView; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/BootReceiver.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.util.Log; 7 | import android.os.Build; 8 | 9 | import com.polar.nextcloudservices.Services.NotificationPollService; 10 | import com.polar.nextcloudservices.Services.NotificationServiceController; 11 | import com.polar.nextcloudservices.Services.Settings.ServiceSettings; 12 | 13 | 14 | public class BootReceiver extends BroadcastReceiver { 15 | 16 | private void startService(Context context){ 17 | NotificationServiceController controller = 18 | new NotificationServiceController(new ServiceSettings(context)); 19 | controller.startService(context); 20 | } 21 | 22 | @Override 23 | public void onReceive(Context context, Intent intent) { 24 | if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { 25 | startService(context); 26 | Log.i("BootReceiver", "received boot completed"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Config.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices; 2 | 3 | public class Config { 4 | public static final String NotificationEventAction = "com.polar.nextcloudservices.Notifications.NOTIFICATION_EVENT"; 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/ContributorDetails.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices; 2 | 3 | public class ContributorDetails { 4 | public String Name; 5 | public String contribution; 6 | public String imageUrl; 7 | public String github_name; 8 | 9 | public ContributorDetails(String name, String contribution, String imageUrl, String github_name) { 10 | this.Name = name; 11 | this.contribution = contribution; 12 | this.imageUrl = imageUrl; 13 | this.github_name = github_name; 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/CreditsActivity.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | import android.view.View; 8 | import android.widget.AdapterView; 9 | import android.widget.ListView; 10 | 11 | import androidx.appcompat.app.AppCompatActivity; 12 | import androidx.browser.customtabs.CustomTabsIntent; 13 | 14 | import com.polar.nextcloudservices.Adapters.CreditsAdapter; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | public class CreditsActivity extends AppCompatActivity { 20 | 21 | private final String TAG = "CreditsActivity"; 22 | List details = new ArrayList<>(); 23 | String[] licenses; 24 | String[] urls; 25 | String[] owner_Name; 26 | String[] owner_github_Name; 27 | String[] owner_github_image; 28 | 29 | @Override 30 | protected void onCreate(Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | setTitle("Credits"); 33 | setContentView(R.layout.activity_credits); 34 | licenses = getResources().getStringArray(R.array.oss_libs); 35 | urls = getResources().getStringArray(R.array.oss_libs_links); 36 | owner_Name = getResources().getStringArray(R.array.oss_libs_owner_name); 37 | owner_github_Name = getResources().getStringArray(R.array.oss_libs_owner_github_name); 38 | owner_github_image = getResources().getStringArray(R.array.oss_libs_owner_Img); 39 | 40 | details_add(); 41 | ListView mListView = (ListView) findViewById(R.id.oss_licenses_list); 42 | if (mListView == null) { 43 | Log.wtf(TAG, "ListView is null!"); 44 | throw new RuntimeException("ListView should not be null!"); 45 | } 46 | CreditsAdapter aAdapter = new CreditsAdapter(this, R.layout.credits_contributer, details); 47 | mListView.setAdapter(aAdapter); 48 | mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 49 | 50 | public void onItemClick(AdapterView parent, View view, int position, long id) { 51 | CustomTabsIntent browserIntent = new CustomTabsIntent.Builder() 52 | .setUrlBarHidingEnabled(true) 53 | .setShowTitle(false) 54 | .setStartAnimations(parent.getContext(), android.R.anim.fade_in, android.R.anim.fade_out) 55 | .setExitAnimations(parent.getContext(), android.R.anim.fade_in, android.R.anim.fade_out) 56 | .setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM) 57 | .setShareState(CustomTabsIntent.SHARE_STATE_OFF) 58 | .build(); 59 | browserIntent.launchUrl(parent.getContext(), Uri.parse(urls[position])); 60 | } 61 | }); 62 | } 63 | 64 | // A function to add the contributor details in details(Arraylist) which is to be shown in credits activity 65 | public void details_add() { 66 | for (int i = 0; i < owner_Name.length; ++i) { 67 | details.add(new ContributorDetails(owner_Name[i], licenses[i], owner_github_image[i], owner_github_Name[i])); 68 | } 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/NSGlideModule.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices; 2 | 3 | import com.bumptech.glide.annotation.GlideModule; 4 | import com.bumptech.glide.module.AppGlideModule; 5 | 6 | //This is to silence Glides' warning about absence of GlideModule 7 | @GlideModule 8 | public final class NSGlideModule extends AppGlideModule {} -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/AbstractNotificationProcessor.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification; 2 | 3 | import android.app.NotificationManager; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | 7 | import androidx.core.app.NotificationCompat; 8 | 9 | import org.json.JSONObject; 10 | 11 | public interface AbstractNotificationProcessor { 12 | /** 13 | * @param id Notification ID in Nextcloud 14 | * @param builderResult a notification builder result which should be updated 15 | * @param manager An Android notification manager service 16 | * @param rawNotification JSON representation of notification 17 | * @param context Android Context object for interacting with Android 18 | * @param controller object which is used for coordinating all notification activity 19 | * @return a result of builder 20 | * @throws Exception throws any exception which occured in notification processors 21 | */ 22 | NotificationBuilderResult updateNotification(int id, NotificationBuilderResult builderResult, 23 | NotificationManager manager, 24 | JSONObject rawNotification, 25 | Context context, NotificationController controller) throws Exception; 26 | 27 | /** 28 | * @param event event that occured 29 | * @param intent intent triggered by event 30 | * @param controller object which is used for coordinating all notification activity 31 | */ 32 | void onNotificationEvent(NotificationEvent event, Intent intent, 33 | NotificationController controller); 34 | 35 | /** 36 | * @return Priority of notification processor 37 | */ 38 | int getPriority(); 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/NotificationBroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.util.Log; 7 | 8 | public class NotificationBroadcastReceiver extends BroadcastReceiver { 9 | private final NotificationEventReceiver mNotificationEventReceiver; 10 | private static final String TAG = "Notification.NotificationBroadcastReceiver"; 11 | public NotificationBroadcastReceiver(NotificationEventReceiver notificationEventReceiver){ 12 | mNotificationEventReceiver = notificationEventReceiver; 13 | } 14 | @Override 15 | public void onReceive(Context context, Intent intent) { 16 | Log.d(TAG, "Received notification event"); 17 | NotificationEvent event_type = (NotificationEvent) intent.getSerializableExtra("notification_event"); 18 | mNotificationEventReceiver.onNotificationEvent(event_type, intent); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/NotificationBuilder.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification; 2 | 3 | import android.app.NotificationManager; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.util.Log; 7 | 8 | import androidx.core.app.NotificationCompat; 9 | 10 | import org.json.JSONObject; 11 | 12 | import java.util.Vector; 13 | 14 | public class NotificationBuilder { 15 | private final Vector processors; 16 | private final static String TAG = "Notification.NotificationBuilder"; 17 | 18 | public NotificationBuilder(){ 19 | processors = new Vector<>(); 20 | } 21 | 22 | public NotificationBuilderResult buildNotification(int id, JSONObject rawNotification, Context context, 23 | NotificationController controller) throws Exception { 24 | NotificationManager mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 25 | NotificationCompat.Builder builder = new NotificationCompat.Builder(context, rawNotification.getString("app")); 26 | NotificationBuilderResult result = new NotificationBuilderResult(builder); 27 | for(int i=0; i=processor.getPriority()){ 42 | break; 43 | } 44 | } 45 | processors.insertElementAt(processor, place); 46 | } 47 | 48 | public void onNotificationEvent(NotificationEvent event, Intent intent, 49 | NotificationController service) { 50 | for(AbstractNotificationProcessor processor: processors){ 51 | processor.onNotificationEvent(event, intent, service); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/NotificationBuilderResult.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification; 2 | 3 | import android.app.Notification; 4 | import android.os.Bundle; 5 | 6 | import androidx.core.app.NotificationCompat; 7 | 8 | public class NotificationBuilderResult { 9 | public NotificationControllerExtData extraData; 10 | public NotificationCompat.Builder builder; 11 | 12 | public NotificationBuilderResult(NotificationCompat.Builder builder){ 13 | this.builder = builder; 14 | this.extraData = new NotificationControllerExtData(); 15 | } 16 | 17 | public Notification getNotification(){ 18 | return builder.build(); 19 | } 20 | 21 | public NotificationControllerExtData getExtraData(){ 22 | return extraData; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/NotificationConfig.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification; 2 | 3 | import com.polar.nextcloudservices.Notification.Processors.ActionsNotificationProcessor; 4 | import com.polar.nextcloudservices.Notification.Processors.basic.BasicNotificationProcessor; 5 | import com.polar.nextcloudservices.Notification.Processors.OpenBrowserProcessor; 6 | import com.polar.nextcloudservices.Notification.Processors.spreed.NextcloudTalkProcessor; 7 | 8 | public class NotificationConfig { 9 | public static final AbstractNotificationProcessor[] NOTIFICATION_PROCESSORS = { 10 | new BasicNotificationProcessor(), 11 | new NextcloudTalkProcessor(), 12 | new OpenBrowserProcessor(), 13 | new ActionsNotificationProcessor() 14 | }; 15 | 16 | public static final String NOTIFICATION_CONTROLLER_EXT_DATA_KEY = "NotificationControllerExtData"; 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/NotificationController.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification; 2 | 3 | import static android.content.Context.NOTIFICATION_SERVICE; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.app.Notification; 7 | import android.app.NotificationChannel; 8 | import android.app.NotificationManager; 9 | import android.content.Context; 10 | import android.content.Intent; 11 | import android.content.IntentFilter; 12 | import android.os.Build; 13 | import android.service.notification.StatusBarNotification; 14 | import android.util.Log; 15 | import android.widget.Toast; 16 | 17 | import androidx.annotation.NonNull; 18 | import androidx.core.app.NotificationCompat; 19 | 20 | import com.polar.nextcloudservices.API.INextcloudAbstractAPI; 21 | import com.polar.nextcloudservices.Config; 22 | import com.polar.nextcloudservices.R; 23 | import com.polar.nextcloudservices.Services.Settings.ServiceSettings; 24 | import com.polar.nextcloudservices.Services.Status.Status; 25 | import com.polar.nextcloudservices.Services.Status.StatusCheckable; 26 | 27 | import org.json.JSONArray; 28 | import org.json.JSONObject; 29 | 30 | import java.util.HashSet; 31 | import java.util.NoSuchElementException; 32 | 33 | public class NotificationController implements NotificationEventReceiver, StatusCheckable { 34 | private final HashSet active_notifications = new HashSet<>(); 35 | private final NotificationBuilder mNotificationBuilder; 36 | private final Context mContext; 37 | private String mStatusString = "Updating settings"; 38 | private boolean isLastSendSuccessful = false; 39 | private static final String TAG = "Notification.NotificationController"; 40 | private final NotificationManager mNotificationManager; 41 | private final ServiceSettings mServiceSettings; 42 | 43 | @SuppressLint("UnspecifiedRegisterReceiverFlag") 44 | public NotificationController(Context context, ServiceSettings settings) { 45 | mNotificationBuilder = new NotificationBuilder(); 46 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU){ 47 | context.getApplicationContext().registerReceiver( 48 | new NotificationBroadcastReceiver(this), 49 | new IntentFilter(Config.NotificationEventAction), 50 | Context.RECEIVER_NOT_EXPORTED); 51 | } else { 52 | context.getApplicationContext().registerReceiver( 53 | new NotificationBroadcastReceiver(this), 54 | new IntentFilter(Config.NotificationEventAction)); 55 | } 56 | mContext = context; 57 | mNotificationManager = 58 | (NotificationManager) mContext.getSystemService(NOTIFICATION_SERVICE); 59 | mServiceSettings = settings; 60 | registerNotificationProcessors(); 61 | } 62 | 63 | @NonNull 64 | public Notification getServiceNotification(){ 65 | //Create background service notifcation 66 | String channelId = "__internal_backgorund_polling"; 67 | NotificationManager mNotificationManager = 68 | (NotificationManager) mContext.getSystemService(NOTIFICATION_SERVICE); 69 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 70 | NotificationChannel channel = new NotificationChannel(channelId, 71 | "Background polling", NotificationManager.IMPORTANCE_LOW); 72 | mNotificationManager.createNotificationChannel(channel); 73 | } 74 | //Build notification 75 | NotificationCompat.Builder mBuilder = 76 | new NotificationCompat.Builder(mContext, channelId) 77 | .setSmallIcon(R.drawable.ic_logo) 78 | .setContentTitle(mContext.getString(R.string.app_name)) 79 | .setPriority(-2) 80 | .setOnlyAlertOnce(true) 81 | .setContentText("Background connection notification"); 82 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 83 | mBuilder.setChannelId(channelId); 84 | } 85 | return mBuilder.build(); 86 | } 87 | 88 | private void registerNotificationProcessors(){ 89 | for(AbstractNotificationProcessor processor : NotificationConfig.NOTIFICATION_PROCESSORS){ 90 | mNotificationBuilder.addProcessor(processor); 91 | } 92 | } 93 | 94 | private void sendNotification(int notification_id, JSONObject notification){ 95 | Log.d(TAG, "Sending notification:" + notification_id); 96 | active_notifications.add(notification_id); 97 | //FIXME: In worst case too many threads can be run 98 | Thread thread = new Thread(() -> { 99 | NotificationBuilderResult builderResult; 100 | Notification aNotification; 101 | int n_id = notification_id; 102 | try { 103 | builderResult = mNotificationBuilder.buildNotification(n_id, 104 | notification, mContext, this); 105 | aNotification = builderResult.getNotification(); 106 | NotificationControllerExtData data = builderResult.getExtraData(); 107 | if(data.needOverrideId()){ 108 | n_id = data.getNotificationId(); 109 | Log.d(TAG, "Overriding " + notification_id + " by " + n_id); 110 | } 111 | } catch (Exception e) { 112 | Log.e(TAG, "Failed to parse notification"); 113 | e.printStackTrace(); 114 | return ; 115 | } 116 | Log.d(TAG, "Will post notification now"); 117 | mNotificationManager.notify(n_id, aNotification); 118 | }); 119 | thread.start(); 120 | } 121 | 122 | public void removeNotification(int notification_id){ 123 | if(notification_id < 0){ 124 | Log.w(TAG, "Got notification id which is negative, ignoring request"); 125 | return; 126 | } 127 | Log.d(TAG, "Removing notification " + Integer.valueOf(notification_id).toString()); 128 | mNotificationManager.cancel(notification_id); 129 | synchronized (active_notifications) { 130 | active_notifications.remove(notification_id); 131 | } 132 | } 133 | 134 | public void onNotificationsUpdated(JSONObject response){ 135 | synchronized (active_notifications) { 136 | try { 137 | HashSet remove_notifications = new HashSet<>(active_notifications); 138 | int notification_id; 139 | JSONArray notifications = response.getJSONObject("ocs").getJSONArray("data"); 140 | for (int i = 0; i < notifications.length(); ++i) { 141 | JSONObject notification = notifications.getJSONObject(i); 142 | notification_id = notification.getInt("notification_id"); 143 | remove_notifications.remove(notification_id); 144 | if (!active_notifications.contains(notification_id)) { 145 | //Handle notification 146 | sendNotification(notification_id, notification); 147 | } 148 | } 149 | for (int remove_id : remove_notifications) { 150 | removeNotification(remove_id); 151 | } 152 | isLastSendSuccessful = true; 153 | } catch (Exception e) { 154 | mStatusString = "Disconnected: " + e.getLocalizedMessage(); 155 | isLastSendSuccessful = false; 156 | e.printStackTrace(); 157 | } 158 | } 159 | } 160 | 161 | public void onNotificationEvent(NotificationEvent event, Intent intent){ 162 | mNotificationBuilder.onNotificationEvent(event, intent, this); 163 | } 164 | 165 | @Override 166 | public Status getStatus(Context context) { 167 | if(!isLastSendSuccessful){ 168 | return Status.Failed(mStatusString); 169 | } 170 | return Status.Ok(); 171 | } 172 | 173 | public void tellActionRequestFailed(){ 174 | Toast.makeText(mContext, R.string.quick_action_failed, Toast.LENGTH_LONG).show(); 175 | } 176 | 177 | public Notification getNotificationById(int id) throws NoSuchElementException { 178 | for(StatusBarNotification notification: mNotificationManager.getActiveNotifications()){ 179 | if(notification.getId() == id){ 180 | return notification.getNotification(); 181 | } 182 | } 183 | throw new NoSuchElementException("Can not find notification with specified id: " + id); 184 | } 185 | 186 | public void postNotification(int id, Notification notification){ 187 | mNotificationManager.notify(id, notification); 188 | } 189 | 190 | public INextcloudAbstractAPI getAPI(){ 191 | return mServiceSettings.getAPIFromSettings(); 192 | } 193 | 194 | public Context getContext(){ 195 | return mContext; 196 | } 197 | 198 | public ServiceSettings getServiceSettings(){ 199 | return mServiceSettings; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/NotificationControllerExtData.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification; 2 | 3 | import android.os.Bundle; 4 | import android.os.Parcel; 5 | import android.os.Parcelable; 6 | import android.util.Log; 7 | 8 | public class NotificationControllerExtData implements Parcelable { 9 | private static final String TAG = "Notification.NotificationControllerExtData"; 10 | private boolean id_override = false; 11 | private int notification_id_override = -1; 12 | 13 | protected NotificationControllerExtData(Parcel in) { 14 | id_override = in.readByte() != 0; 15 | notification_id_override = in.readInt(); 16 | } 17 | 18 | public NotificationControllerExtData(){ 19 | /* STUB */ 20 | } 21 | 22 | @Override 23 | public void writeToParcel(Parcel dest, int flags) { 24 | dest.writeByte((byte) (id_override ? 1 : 0)); 25 | dest.writeInt(notification_id_override); 26 | } 27 | 28 | @Override 29 | public int describeContents() { 30 | return 0; 31 | } 32 | 33 | public static final Creator CREATOR = new Creator() { 34 | @Override 35 | public NotificationControllerExtData createFromParcel(Parcel in) { 36 | return new NotificationControllerExtData(in); 37 | } 38 | 39 | @Override 40 | public NotificationControllerExtData[] newArray(int size) { 41 | return new NotificationControllerExtData[size]; 42 | } 43 | }; 44 | 45 | public boolean needOverrideId(){ 46 | return id_override; 47 | } 48 | 49 | public int getNotificationId(){ 50 | return notification_id_override; 51 | } 52 | 53 | public void setNotificationIdOverride(int id){ 54 | if(id_override){ 55 | Log.w(TAG, "Overriding notification id " + id + " which is already overriden"); 56 | } 57 | id_override = true; 58 | notification_id_override = id; 59 | } 60 | 61 | public Bundle asBundle(){ 62 | Bundle bundle = new Bundle(); 63 | bundle.putParcelable(NotificationConfig.NOTIFICATION_CONTROLLER_EXT_DATA_KEY, this); 64 | return bundle; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/NotificationEvent.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification; 2 | 3 | public enum NotificationEvent { 4 | NOTIFICATION_EVENT_UNKNOWN(0), 5 | NOTIFICATION_EVENT_DELETE(1), 6 | NOTIFICATION_EVENT_FASTREPLY(2), 7 | NOTIFICATION_EVENT_CUSTOM_ACTION(3); 8 | public final int value; 9 | 10 | NotificationEvent(int _value){ 11 | value = _value; 12 | } 13 | 14 | static public NotificationEvent fromInt(int code){ 15 | switch (code){ 16 | case 0: 17 | return NOTIFICATION_EVENT_UNKNOWN; 18 | case 1: 19 | return NOTIFICATION_EVENT_DELETE; 20 | case 2: 21 | return NOTIFICATION_EVENT_FASTREPLY; 22 | case 3: 23 | return NOTIFICATION_EVENT_CUSTOM_ACTION; 24 | default: 25 | throw new RuntimeException("Bad event code: " + code); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/NotificationEventReceiver.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification; 2 | 3 | import android.content.Intent; 4 | 5 | public interface NotificationEventReceiver { 6 | void onNotificationEvent(NotificationEvent event_type, Intent intent); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/Processors/ActionsNotificationProcessor.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification.Processors; 2 | 3 | import static com.polar.nextcloudservices.Notification.NotificationEvent.NOTIFICATION_EVENT_CUSTOM_ACTION; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.app.NotificationManager; 7 | import android.app.PendingIntent; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.os.Build; 11 | import android.util.Log; 12 | 13 | import androidx.annotation.NonNull; 14 | import androidx.core.app.NotificationCompat; 15 | 16 | import com.polar.nextcloudservices.Config; 17 | import com.polar.nextcloudservices.Notification.AbstractNotificationProcessor; 18 | import com.polar.nextcloudservices.Notification.NotificationBuilderResult; 19 | import com.polar.nextcloudservices.Notification.NotificationController; 20 | import com.polar.nextcloudservices.Notification.NotificationEvent; 21 | import com.polar.nextcloudservices.Utils.CommonUtil; 22 | 23 | import org.json.JSONArray; 24 | import org.json.JSONException; 25 | import org.json.JSONObject; 26 | 27 | public class ActionsNotificationProcessor implements AbstractNotificationProcessor { 28 | 29 | public static final int priority = 2; 30 | private static final String TAG = "Notification.Processors.ActionsNotificationProcessor"; 31 | private static final String[] IGNORED_APPS = {"spreed"}; 32 | 33 | @SuppressLint("UnspecifiedImmutableFlag") 34 | private static PendingIntent getCustomActionIntent(Context context, 35 | JSONObject action, int requestCode){ 36 | Intent intent = new Intent(); 37 | intent.setAction(Config.NotificationEventAction); 38 | try { 39 | intent.putExtra("notification_event", NOTIFICATION_EVENT_CUSTOM_ACTION); 40 | String link = action.getString("link"); 41 | final String type = action.getString("type"); 42 | link = CommonUtil.cleanUpURLIfNeeded(link); 43 | if(link == null){ 44 | Log.e(TAG, "Nextcloud provided bad link for action"); 45 | return null; 46 | } 47 | intent.putExtra("action_link", link); 48 | intent.putExtra("action_method", type); 49 | intent.setPackage(context.getPackageName()); // Issue 78 --> https://developer.android.com/about/versions/14/behavior-changes-14?hl=en#safer-intents 50 | } catch (JSONException e) { 51 | Log.e(TAG, "Can not get link or method from action provided by Nextcloud API"); 52 | e.printStackTrace(); 53 | return null; 54 | } 55 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 56 | return PendingIntent.getBroadcast( 57 | context, 58 | requestCode, 59 | intent, 60 | PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT 61 | ); 62 | }else{ 63 | return PendingIntent.getBroadcast( 64 | context, 65 | requestCode, 66 | intent, 67 | PendingIntent.FLAG_UPDATE_CURRENT 68 | ); 69 | } 70 | } 71 | 72 | @Override 73 | public NotificationBuilderResult updateNotification(int id, NotificationBuilderResult builderResult, 74 | NotificationManager manager, 75 | @NonNull JSONObject rawNotification, 76 | Context context, 77 | NotificationController controller) throws Exception { 78 | final String appName = rawNotification.getString("app"); 79 | if (CommonUtil.isInArray(appName, IGNORED_APPS)) { 80 | Log.d(TAG, appName + " is ignored"); 81 | return builderResult; 82 | } else { 83 | Log.d(TAG, appName + " is not ignored"); 84 | } 85 | if(!rawNotification.has("actions")){ 86 | return builderResult; 87 | } 88 | JSONArray actions = rawNotification.getJSONArray("actions"); 89 | final int n_actions = actions.length(); 90 | for(int i = 0; i < n_actions; ++i){ 91 | JSONObject action = actions.getJSONObject(i); 92 | PendingIntent actionPendingIntent = getCustomActionIntent(context, action, i); 93 | if(actionPendingIntent == null){ 94 | Log.w(TAG, "Can not create action for notification"); 95 | return builderResult; 96 | } 97 | final String actionTitle = action.getString("label"); 98 | NotificationCompat.Action notificationAction = new NotificationCompat.Action.Builder( 99 | null, 100 | actionTitle, actionPendingIntent) 101 | .build(); 102 | builderResult.builder.addAction(notificationAction); 103 | } 104 | return builderResult; 105 | } 106 | 107 | @Override 108 | public void onNotificationEvent(NotificationEvent event, Intent intent, 109 | NotificationController controller) { 110 | if(event == NOTIFICATION_EVENT_CUSTOM_ACTION){ 111 | final String link = intent.getStringExtra("action_link"); 112 | final String method = intent.getStringExtra("action_method"); 113 | Log.d(TAG, method + " " + link); 114 | Thread thread = new Thread(() -> { 115 | try { 116 | controller.getAPI().sendAction(link, method); 117 | } catch (Exception e) { 118 | Log.e(TAG, e.toString()); 119 | controller.tellActionRequestFailed(); 120 | } 121 | }); 122 | thread.start(); 123 | } 124 | } 125 | 126 | @Override 127 | public int getPriority() { 128 | return priority; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/Processors/OpenBrowserProcessor.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification.Processors; 2 | 3 | // This processor is default processor for user click event 4 | // It is used to open web page and has priority 1 5 | // So it is executed first and can be overriden by per-app processors 6 | 7 | 8 | import android.annotation.SuppressLint; 9 | import android.app.NotificationManager; 10 | import android.app.PendingIntent; 11 | import android.content.Context; 12 | import android.content.Intent; 13 | import android.net.Uri; 14 | import android.os.Build; 15 | import android.util.Log; 16 | 17 | import androidx.browser.customtabs.CustomTabsIntent; 18 | 19 | import com.polar.nextcloudservices.Notification.AbstractNotificationProcessor; 20 | import com.polar.nextcloudservices.Notification.NotificationBuilderResult; 21 | import com.polar.nextcloudservices.Notification.NotificationController; 22 | import com.polar.nextcloudservices.Notification.NotificationEvent; 23 | 24 | import org.json.JSONException; 25 | import org.json.JSONObject; 26 | 27 | public class OpenBrowserProcessor implements AbstractNotificationProcessor { 28 | public final int priority = 1; 29 | private static final String TAG = "Notification.Processors.OpenBrowserProcessor"; 30 | 31 | @Override 32 | public NotificationBuilderResult updateNotification(int id, NotificationBuilderResult builderResult, 33 | NotificationManager manager, 34 | JSONObject rawNotification, 35 | Context context, NotificationController controller) throws JSONException { 36 | if (!rawNotification.has("link")) { 37 | return builderResult; 38 | } 39 | 40 | Log.d(TAG, "Setting link for browser opening"); 41 | 42 | CustomTabsIntent browserIntent = new CustomTabsIntent.Builder() 43 | .setUrlBarHidingEnabled(true) 44 | .setShowTitle(false) 45 | .setStartAnimations(context, android.R.anim.fade_in, android.R.anim.fade_out) 46 | .setExitAnimations(context, android.R.anim.fade_in, android.R.anim.fade_out) 47 | .setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM) 48 | .setShareState(CustomTabsIntent.SHARE_STATE_OFF) 49 | .build(); 50 | browserIntent.intent.setData(Uri.parse(rawNotification.getString("link"))); 51 | 52 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 53 | builderResult.builder = builderResult.builder.setContentIntent(PendingIntent.getActivity(context, 0, 54 | browserIntent.intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT)); 55 | }else{ 56 | builderResult.builder = builderResult.builder.setContentIntent(PendingIntent.getActivity(context, 0, 57 | browserIntent.intent, PendingIntent.FLAG_UPDATE_CURRENT)); 58 | } 59 | return builderResult; 60 | } 61 | 62 | @Override 63 | public void onNotificationEvent(NotificationEvent event, Intent intent, NotificationController controller) { 64 | 65 | } 66 | 67 | @Override 68 | public int getPriority() { 69 | return priority; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/Processors/basic/AppNameMapper.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification.Processors.basic; 2 | 3 | import android.content.Context; 4 | 5 | import com.polar.nextcloudservices.R; 6 | import com.polar.nextcloudservices.Utils.CommonUtil; 7 | 8 | import java.util.Collections; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * Maps Nextcloud app name to prettified name of app 14 | */ 15 | public class AppNameMapper { 16 | /** 17 | * Array of apps which have mapped names 18 | */ 19 | public static final String[] MAPPABLE_APPS = {"spreed", 20 | "updatenotification", "twofactor_nextcloud_notification"}; 21 | 22 | /** 23 | * A map mapping the mappable apps to resource id 24 | */ 25 | public static final Map APP_TO_RESID_MAPPING; 26 | static { 27 | // Put app mapping here 28 | Map aMap = new HashMap<>(); 29 | aMap.put("spreed", R.string.spreed_name); 30 | aMap.put("updatenotification", R.string.updatenotification_name); 31 | aMap.put("twofactor_nextcloud_notification", R.string.twofactor_nextcloud_notification_name); 32 | APP_TO_RESID_MAPPING = Collections.unmodifiableMap(aMap); 33 | } 34 | 35 | 36 | /** 37 | * @param appName Nextcloud app name 38 | * @return whether mapping to prettified name exists for provided app name 39 | */ 40 | public static boolean isAppMappable(String appName){ 41 | return CommonUtil.isInArray(appName, MAPPABLE_APPS); 42 | } 43 | 44 | /** 45 | * @param context Android context 46 | * @param appName Nextcloud app name 47 | * @return prettified app name from mapping 48 | * @throws RuntimeException if app has no prettified mapping 49 | */ 50 | public static String getPrettifiedAppNameFromMapping(Context context, String appName){ 51 | Integer result = APP_TO_RESID_MAPPING.get(appName); 52 | if(result == null){ 53 | throw new RuntimeException("No entry for mapping app " + appName + " found"); 54 | } 55 | return context.getString(result); 56 | } 57 | 58 | /** 59 | * Get prettified app name from mapping if exists, tries to prettify if not 60 | * @param context Android context 61 | * @param appName Nextcloud app name 62 | * @return prettified name of app 63 | */ 64 | public static String getPrettifiedAppName(Context context, String appName){ 65 | if(isAppMappable(appName)){ 66 | return getPrettifiedAppNameFromMapping(context, appName); 67 | } 68 | String[] parts = appName.split("_"); 69 | StringBuilder nice_name = new StringBuilder(); 70 | for (String part : parts) { 71 | nice_name.append(part).append(" "); 72 | } 73 | String result = nice_name.toString(); 74 | result = Character.toUpperCase(result.charAt(0)) + result.substring(1); 75 | return result; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/Processors/basic/BasicNotificationProcessor.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification.Processors.basic; 2 | 3 | import static com.polar.nextcloudservices.Notification.NotificationEvent.NOTIFICATION_EVENT_DELETE; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.app.NotificationChannel; 7 | import android.app.NotificationManager; 8 | import android.app.PendingIntent; 9 | import android.content.Context; 10 | import android.content.Intent; 11 | import android.os.Build; 12 | import android.util.Log; 13 | 14 | import com.polar.nextcloudservices.Notification.AbstractNotificationProcessor; 15 | import com.polar.nextcloudservices.Config; 16 | import com.polar.nextcloudservices.Notification.NotificationBuilderResult; 17 | import com.polar.nextcloudservices.Notification.NotificationController; 18 | import com.polar.nextcloudservices.Notification.NotificationEvent; 19 | import com.polar.nextcloudservices.R; 20 | import com.polar.nextcloudservices.Services.Settings.ServiceSettings; 21 | 22 | import org.json.JSONException; 23 | import org.json.JSONObject; 24 | 25 | import java.text.ParseException; 26 | import java.text.SimpleDateFormat; 27 | import java.util.Date; 28 | 29 | public class BasicNotificationProcessor implements AbstractNotificationProcessor { 30 | public final int priority = 0; 31 | private final static String TAG = "Notification.Processors.BasicNotificationProcessor"; 32 | 33 | public int iconByApp(String appName) { 34 | switch (appName) { 35 | case "spreed": 36 | return R.drawable.ic_icon_foreground; 37 | case "deck": 38 | return R.drawable.ic_deck; 39 | case "twofactor_nextcloud_notification": 40 | return android.R.drawable.ic_partial_secure; 41 | default: 42 | return R.drawable.ic_logo; 43 | } 44 | } 45 | 46 | 47 | //@SuppressLint("UnspecifiedImmutableFlag") 48 | private PendingIntent createNotificationDeleteIntent(Context context, int id) { 49 | Intent intent = new Intent(); 50 | intent.setAction(Config.NotificationEventAction); 51 | intent.putExtra("notification_id", id); 52 | intent.putExtra("notification_event", NOTIFICATION_EVENT_DELETE); 53 | intent.setPackage(context.getPackageName()); 54 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 55 | return PendingIntent.getBroadcast( 56 | context, 57 | id, 58 | intent, 59 | PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE 60 | ); 61 | } else { 62 | return PendingIntent.getBroadcast( 63 | context, 64 | id, 65 | intent, 66 | PendingIntent.FLAG_UPDATE_CURRENT 67 | ); 68 | } 69 | } 70 | 71 | @Override 72 | public NotificationBuilderResult updateNotification(int id, NotificationBuilderResult builderResult, 73 | NotificationManager manager, 74 | JSONObject rawNotification, 75 | Context context, 76 | NotificationController controller) throws JSONException { 77 | final ServiceSettings settings = new ServiceSettings(context); 78 | final boolean removeOnDismiss = settings.isRemoveOnDismissEnabled(); 79 | final String app = AppNameMapper.getPrettifiedAppName(context, 80 | rawNotification.getString("app")); 81 | final String title = rawNotification.getString("subject"); 82 | final String text = rawNotification.getString("message"); 83 | final String app_name = rawNotification.getString("app"); 84 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { 85 | NotificationChannel channel = new NotificationChannel(app_name, app, NotificationManager.IMPORTANCE_HIGH); 86 | Log.i(TAG, "Creating channel " + app_name); 87 | manager.createNotificationChannel(channel); 88 | } 89 | SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); 90 | final String dateStr = rawNotification.getString("datetime"); 91 | long unixTime = 0; 92 | try { 93 | Date date = format.parse(dateStr); 94 | if(date == null){ 95 | throw new ParseException("Date was not parsed: result is null", 0); 96 | } 97 | unixTime = date.getTime(); 98 | } catch (ParseException e) { 99 | e.printStackTrace(); 100 | } 101 | builderResult.builder = builderResult.builder.setSmallIcon(iconByApp(app_name)) 102 | .setContentTitle(title) 103 | .setAutoCancel(true) 104 | .setContentText(text) 105 | .setChannelId(app_name); 106 | if(unixTime != 0){ 107 | builderResult.builder.setWhen(unixTime); 108 | }else{ 109 | Log.w(TAG, "unixTime is 0, maybe parse failure?"); 110 | } 111 | if(removeOnDismiss){ 112 | Log.d(TAG, "Adding intent for delete notification event"); 113 | builderResult.builder = builderResult.builder.setDeleteIntent(createNotificationDeleteIntent(context, 114 | rawNotification.getInt("notification_id"))); 115 | } 116 | return builderResult; 117 | } 118 | 119 | @Override 120 | public void onNotificationEvent(NotificationEvent event, Intent intent, 121 | NotificationController controller) { 122 | if(event != NOTIFICATION_EVENT_DELETE){ 123 | return; 124 | } 125 | int id = intent.getIntExtra("notification_id", -1); 126 | Log.d(TAG, "Should remove notification " + id); 127 | if(id < 0){ 128 | Log.wtf(TAG, "Notification delete event has not provided an id of notification deleted!"); 129 | } 130 | Thread thread = new Thread(() -> callRemoveNotification(controller, id)); 131 | thread.start(); 132 | } 133 | 134 | private void callRemoveNotification(NotificationController controller, int id){ 135 | controller.getAPI().removeNotification(id); 136 | } 137 | 138 | @Override 139 | public int getPriority() { 140 | return priority; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/Processors/spreed/NextcloudTalkProcessor.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification.Processors.spreed; 2 | 3 | import static com.polar.nextcloudservices.Notification.NotificationEvent.NOTIFICATION_EVENT_DELETE; 4 | import static com.polar.nextcloudservices.Notification.NotificationEvent.NOTIFICATION_EVENT_FASTREPLY; 5 | 6 | import android.annotation.SuppressLint; 7 | import android.app.Notification; 8 | import android.app.NotificationManager; 9 | import android.app.PendingIntent; 10 | import android.content.Context; 11 | import android.content.Intent; 12 | import android.content.pm.PackageManager; 13 | import android.graphics.Bitmap; 14 | import android.net.Uri; 15 | import android.os.Build; 16 | import android.os.Bundle; 17 | import android.util.Log; 18 | 19 | import androidx.annotation.NonNull; 20 | import androidx.browser.customtabs.CustomTabsIntent; 21 | import androidx.core.app.NotificationCompat; 22 | import androidx.core.app.Person; 23 | import androidx.core.app.RemoteInput; 24 | import androidx.core.graphics.drawable.IconCompat; 25 | 26 | import com.polar.nextcloudservices.API.INextcloudAbstractAPI; 27 | import com.polar.nextcloudservices.Config; 28 | import com.polar.nextcloudservices.Notification.AbstractNotificationProcessor; 29 | import com.polar.nextcloudservices.Notification.NotificationBuilderResult; 30 | import com.polar.nextcloudservices.Notification.NotificationController; 31 | import com.polar.nextcloudservices.Notification.NotificationEvent; 32 | import com.polar.nextcloudservices.Notification.Processors.spreed.chat.Chat; 33 | import com.polar.nextcloudservices.Notification.Processors.spreed.chat.ChatController; 34 | import com.polar.nextcloudservices.Notification.Processors.spreed.chat.ChatMessage; 35 | import com.polar.nextcloudservices.R; 36 | import com.polar.nextcloudservices.Services.Settings.ServiceSettings; 37 | import com.polar.nextcloudservices.Utils.CommonUtil; 38 | 39 | import org.json.JSONException; 40 | import org.json.JSONObject; 41 | 42 | import java.io.IOException; 43 | import java.text.ParseException; 44 | import java.text.SimpleDateFormat; 45 | import java.util.Date; 46 | import java.util.Objects; 47 | 48 | public class NextcloudTalkProcessor implements AbstractNotificationProcessor { 49 | public final int priority = 2; 50 | private static final String TAG = "Notification.Processors.NextcloudTalkProcessor"; 51 | private static final String KEY_TEXT_REPLY = "key_text_reply"; 52 | private final ChatController mChatController; 53 | 54 | public NextcloudTalkProcessor() { 55 | mChatController = new ChatController(); 56 | } 57 | 58 | @SuppressLint("UnspecifiedImmutableFlag") 59 | static private PendingIntent getReplyIntent(Context context, 60 | @NonNull JSONObject rawNotification) throws JSONException { 61 | Intent intent = new Intent(); 62 | int notification_id = rawNotification.getInt("notification_id"); 63 | intent.setAction(Config.NotificationEventAction); 64 | intent.putExtra("notification_id", rawNotification.getInt("notification_id")); 65 | intent.putExtra("notification_event", NOTIFICATION_EVENT_FASTREPLY); 66 | String[] link = rawNotification.getString("link").split("/"); // use provided link to extract talk chatroom id 67 | intent.putExtra("talk_chatroom", CommonUtil.cleanUpURLParams(link[link.length-1])); 68 | intent.putExtra("talk_link", CommonUtil.cleanUpURLParams(rawNotification.getString("link"))); 69 | intent.setPackage(context.getPackageName()); // Issue 78 --> https://developer.android.com/about/versions/14/behavior-changes-14?hl=en#safer-intents 70 | 71 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 72 | return PendingIntent.getBroadcast( 73 | context, 74 | notification_id, 75 | intent, 76 | PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT 77 | ); 78 | }else{ 79 | return PendingIntent.getBroadcast( 80 | context, 81 | notification_id, 82 | intent, 83 | PendingIntent.FLAG_UPDATE_CURRENT 84 | ); 85 | } 86 | } 87 | 88 | private Person getUserPerson(){ 89 | Person.Builder builder = new Person.Builder(); 90 | builder.setName("You"); 91 | return builder.build(); 92 | } 93 | 94 | @NonNull 95 | private Person getPersonFromNotification(@NonNull NotificationController controller, 96 | @NonNull JSONObject rawNotification) throws Exception { 97 | Person.Builder builder = new Person.Builder(); 98 | if(rawNotification.getJSONObject("subjectRichParameters").has("user")){ 99 | JSONObject user = rawNotification.getJSONObject("subjectRichParameters") 100 | .getJSONObject("user"); 101 | final String name = user.getString("name"); 102 | final String id = user.getString("id"); 103 | builder.setKey(id).setName(name); 104 | Bitmap image = controller.getAPI().getUserAvatar(id); 105 | IconCompat compat = IconCompat.createWithAdaptiveBitmap(image); 106 | builder.setIcon(compat); 107 | return builder.build(); 108 | }else { 109 | final String key = rawNotification.getString("object_id"); 110 | builder.setKey(key); 111 | final String name = rawNotification.getJSONObject("subjectRichParameters") 112 | .getJSONObject("call").getString("name"); 113 | //NOTE: Nextcloud Talk does not seem to provide ability for setting avatar for calls 114 | // so it is not fetched here 115 | return builder.setName(name).build(); 116 | } 117 | } 118 | 119 | private NotificationCompat.Builder setCustomTabsIntent(Context context, 120 | NotificationCompat.Builder builder, 121 | String link) { 122 | CustomTabsIntent browserIntent = new CustomTabsIntent.Builder() 123 | .setUrlBarHidingEnabled(true) 124 | .setShowTitle(false) 125 | .setStartAnimations(context, android.R.anim.fade_in, android.R.anim.fade_out) 126 | .setExitAnimations(context, android.R.anim.fade_in, android.R.anim.fade_out) 127 | .setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM) 128 | .setShareState(CustomTabsIntent.SHARE_STATE_OFF) 129 | .build(); 130 | browserIntent.intent.setData(Uri.parse(link)); 131 | 132 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 133 | return builder.setContentIntent(PendingIntent.getActivity(context, 0, 134 | browserIntent.intent, 135 | PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT)); 136 | }else{ 137 | return builder.setContentIntent(PendingIntent.getActivity(context, 0, 138 | browserIntent.intent, PendingIntent.FLAG_UPDATE_CURRENT)); 139 | } 140 | } 141 | 142 | private NotificationCompat.Builder setTalkOpenIntent(Context context, 143 | NotificationCompat.Builder builder){ 144 | PackageManager pm = context.getPackageManager(); 145 | if (!CommonUtil.isPackageInstalled("com.nextcloud.talk2", pm)) { 146 | Log.w(TAG, "Expected to find com.nextcloud.talk2 installed, but package was not found"); 147 | return builder; 148 | } 149 | Log.d(TAG, "Setting up talk notification open intent"); 150 | 151 | Intent intent = pm.getLaunchIntentForPackage("com.nextcloud.talk2"); 152 | intent.setPackage(context.getPackageName()); 153 | PendingIntent pending_intent; 154 | pending_intent = PendingIntent.getActivity(context, 0, intent, 155 | PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 156 | return builder.setContentIntent(pending_intent); 157 | } 158 | 159 | private NotificationCompat.Builder setOpenIntent(NotificationController controller, 160 | NotificationCompat.Builder builder, 161 | Context context, String link){ 162 | ServiceSettings settings = controller.getServiceSettings(); 163 | if(settings == null){ 164 | Log.wtf(TAG, "settings is null!"); 165 | throw new RuntimeException("controller.getServiceSettings() returned null"); 166 | } 167 | if(settings.getSpreedOpenedInBrowser()){ 168 | return setCustomTabsIntent(context, builder, link); 169 | } else { 170 | PackageManager pm = context.getPackageManager(); 171 | if (!CommonUtil.isPackageInstalled("com.nextcloud.talk2", pm)) { 172 | Log.w(TAG, "Expected to find com.nextcloud.talk2 installed, but package was not found"); 173 | return setCustomTabsIntent(context, builder, link); 174 | } 175 | return setTalkOpenIntent(context, builder); 176 | } 177 | } 178 | 179 | 180 | private NotificationBuilderResult setMessagingChatStyle(NotificationController controller, 181 | NotificationBuilderResult builderResult, 182 | @NonNull JSONObject rawNotification) throws Exception { 183 | Person person = getPersonFromNotification(controller, rawNotification); 184 | final String room = CommonUtil.cleanUpURLParams(rawNotification.getString("link")); 185 | final String title = rawNotification.getJSONObject("subjectRichParameters") 186 | .getJSONObject("call").getString("name"); 187 | final String text = rawNotification.getString("message"); 188 | int nc_notification_id = rawNotification.getInt("notification_id"); 189 | SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'"); 190 | final String dateStr = rawNotification.getString("datetime"); 191 | long unixTime = 0; 192 | try { 193 | Date date = format.parse(dateStr); 194 | if (date == null) { 195 | throw new ParseException("Date was not parsed: result is null", 0); 196 | } 197 | unixTime = date.getTime(); 198 | } catch (ParseException e) { 199 | e.printStackTrace(); 200 | } 201 | mChatController.onNewMessageReceived(room, text, person, unixTime, nc_notification_id); 202 | NotificationCompat.MessagingStyle style = new NotificationCompat.MessagingStyle(person); 203 | style = mChatController.addChatRoomMessagesToStyle(style, room); 204 | style.setConversationTitle(title); 205 | int notification_id = mChatController.getNotificationIdByRoom(room); 206 | builderResult.extraData.setNotificationIdOverride(notification_id); 207 | builderResult.builder = builderResult.builder.setStyle(style); 208 | return builderResult; 209 | } 210 | 211 | 212 | @SuppressLint("UnspecifiedImmutableFlag") 213 | @Override 214 | public NotificationBuilderResult updateNotification(int id, NotificationBuilderResult builderResult, 215 | NotificationManager manager, 216 | @NonNull JSONObject rawNotification, 217 | Context context, NotificationController controller) throws Exception { 218 | 219 | if (!rawNotification.getString("app").equals("spreed")) { 220 | return builderResult; 221 | } 222 | 223 | Log.d(TAG, "Setting up talk notification"); 224 | 225 | if (rawNotification.has("object_type")) { 226 | if (rawNotification.getString("object_type").equals("chat")) { 227 | Log.d(TAG, "Talk notification of chat type, adding fast reply button"); 228 | String replyLabel = context.getString(R.string.talk_fast_reply); 229 | RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY) 230 | .setLabel(replyLabel) 231 | .build(); 232 | PendingIntent replyPendingIntent = getReplyIntent(context, rawNotification); 233 | final String fastreply_title = context.getString(R.string.talk_fast_reply); 234 | NotificationCompat.Action action = 235 | new NotificationCompat.Action.Builder(R.drawable.ic_reply_icon, 236 | fastreply_title, replyPendingIntent) 237 | .addRemoteInput(remoteInput) 238 | .setAllowGeneratedReplies(true) 239 | .build(); 240 | builderResult.builder.addAction(action); 241 | if (rawNotification.getString("messageRich").equals("{file}") && rawNotification 242 | .getJSONObject("messageRichParameters") 243 | .getJSONObject("file") 244 | .getString("mimetype").startsWith("image/")) { 245 | Bitmap imagePreview = controller.getAPI().getImagePreview(rawNotification 246 | .getJSONObject("messageRichParameters") 247 | .getJSONObject("file").getString("id")); 248 | builderResult.builder.setStyle(new NotificationCompat.BigPictureStyle() 249 | .bigPicture(imagePreview)); 250 | } else { 251 | setMessagingChatStyle(controller, builderResult, 252 | rawNotification); 253 | } 254 | } 255 | } 256 | builderResult.builder = setOpenIntent(controller, builderResult.builder, context, 257 | rawNotification.getString("link")); 258 | return builderResult; 259 | } 260 | 261 | private void onFastReply(Intent intent, NotificationController controller){ 262 | final String chatroom = 263 | CommonUtil.cleanUpURLParams( 264 | Objects.requireNonNull(intent.getStringExtra("talk_chatroom"))); // the string send by spreed is chatroomid 265 | final String chatroom_link = CommonUtil.cleanUpURLParams( 266 | Objects.requireNonNull(intent.getStringExtra("talk_link"))); 267 | final int notification_id = intent.getIntExtra("notification_id", -1); 268 | if (notification_id < 0) { 269 | Log.wtf(TAG, "Bad notification id: " + notification_id); 270 | return; 271 | } 272 | Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); 273 | if (remoteInput == null) { 274 | Log.e(TAG, "Reply event has null reply text"); 275 | return; 276 | } 277 | final String reply = 278 | Objects.requireNonNull(remoteInput.getCharSequence(KEY_TEXT_REPLY)).toString(); 279 | INextcloudAbstractAPI api = controller.getAPI(); 280 | Thread thread = new Thread(() -> { 281 | try { 282 | api.sendTalkReply(chatroom, reply); 283 | appendQuickReply(controller, 284 | mChatController.getNotificationIdByRoom(chatroom_link), reply); 285 | } catch (Exception e) { 286 | Log.e(TAG, e.toString()); 287 | e.printStackTrace(); 288 | ///controller.tellActionRequestFailed(); 289 | } 290 | }); 291 | thread.start(); 292 | } 293 | 294 | private void onDeleteNotification(Intent intent, NotificationController controller){ 295 | //NOTE: we actually can not get here if remove on dismiss disabled 296 | // so we may safely ignore checking settings 297 | final int notification_id = intent.getIntExtra("notification_id", -1); 298 | if(notification_id == -1){ 299 | Log.e(TAG, "Invalid notification id, can not properly handle notification deletion"); 300 | return; 301 | } 302 | Chat chat = mChatController.getChatByNotificationId(notification_id); 303 | if(chat == null){ 304 | Log.wtf(TAG, "Can not find chat by notification id " + notification_id); 305 | return; 306 | } 307 | INextcloudAbstractAPI api = controller.getAPI(); 308 | for(ChatMessage message : chat.messages){ 309 | Thread thread = new Thread(() -> { 310 | try { 311 | api.removeNotification(message.notification_id); 312 | } catch (Exception e) { 313 | Log.e(TAG, e.toString()); 314 | } 315 | }); 316 | thread.start(); 317 | } 318 | mChatController.removeChat(chat); 319 | } 320 | 321 | @Override 322 | public void onNotificationEvent(NotificationEvent event, Intent intent, 323 | NotificationController controller) { 324 | if (event == NOTIFICATION_EVENT_FASTREPLY) { 325 | onFastReply(intent, controller); 326 | } else if(event == NOTIFICATION_EVENT_DELETE){ 327 | onDeleteNotification(intent, controller); 328 | } 329 | } 330 | 331 | private void appendQuickReply(NotificationController controller, 332 | int notification_id, String text){ 333 | Notification notification = controller.getNotificationById(notification_id); 334 | Context context = controller.getContext(); 335 | NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notification); 336 | NotificationCompat.MessagingStyle style = NotificationCompat 337 | .MessagingStyle.extractMessagingStyleFromNotification(notification); 338 | if(style == null){ 339 | Log.wtf(TAG, "appendQuickReply: got null style"); 340 | return; 341 | } 342 | style.addMessage(text, CommonUtil.getTimestamp(), getUserPerson()); 343 | final String room = mChatController.getChatByNotificationId(notification_id).room; 344 | mChatController.onNewMessageReceived(room, text, getUserPerson(), 345 | CommonUtil.getTimestamp(), -1); 346 | notification = builder.setStyle(style).build(); 347 | controller.postNotification(notification_id, notification); 348 | } 349 | 350 | @Override 351 | public int getPriority() { 352 | return priority; 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/Processors/spreed/chat/Chat.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification.Processors.spreed.chat; 2 | 3 | import androidx.core.app.Person; 4 | 5 | import java.util.Vector; 6 | 7 | public class Chat { 8 | public final Vector messages; 9 | public String room; 10 | public Integer nc_notification_id; 11 | 12 | 13 | public Chat(Integer nc_notification_id, String room) { 14 | this.nc_notification_id = nc_notification_id; 15 | messages = new Vector<>(); 16 | this.room = room; 17 | } 18 | 19 | public void onNewMessage(String text, Person person, long timestamp, int nc_notification_id){ 20 | messages.add(new ChatMessage(text, person, timestamp, nc_notification_id)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/Processors/spreed/chat/ChatController.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification.Processors.spreed.chat; 2 | 3 | import android.util.Log; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.core.app.NotificationCompat; 7 | import androidx.core.app.Person; 8 | 9 | import java.util.Collections; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.NoSuchElementException; 13 | 14 | /** 15 | * A generic controller of a chat logic. 16 | * It stores a history of messages per converstation and also allows to cancel, i.e. 17 | * remove conversation by message id. 18 | */ 19 | public class ChatController { 20 | private final HashMap chat_by_room; 21 | private final HashMap chat_by_notification_id; 22 | private final HashMap notification_id_by_room; 23 | 24 | private final static String TAG = "ChatController"; 25 | 26 | public ChatController() { 27 | chat_by_room = new HashMap<>(); 28 | chat_by_notification_id = new HashMap<>(); 29 | notification_id_by_room = new HashMap<>(); 30 | } 31 | 32 | private Chat getChat(String room, int nc_notification_id){ 33 | if(!chat_by_room.containsKey(room)){ 34 | Chat chat = new Chat(nc_notification_id, room); 35 | chat_by_room.put(room, chat); 36 | chat_by_notification_id.put(nc_notification_id, chat); 37 | notification_id_by_room.put(room, nc_notification_id); 38 | } 39 | return chat_by_room.get(room); 40 | } 41 | 42 | public @NonNull Integer getNotificationIdByRoom(String room) throws NoSuchElementException{ 43 | if(!notification_id_by_room.containsKey(room)){ 44 | throw new NoSuchElementException("Can not find room: " + room); 45 | } 46 | return notification_id_by_room.get(room); 47 | } 48 | 49 | public void onNewMessageReceived(String room, String text, 50 | Person person, 51 | long timestamp, int nc_notification_id){ 52 | synchronized (chat_by_room) { 53 | Chat chat = getChat(room, nc_notification_id); 54 | chat.onNewMessage(text, person, timestamp, nc_notification_id); 55 | chat_by_notification_id.put(nc_notification_id, chat); 56 | } 57 | } 58 | 59 | public Chat getChatByRoom(String room){ 60 | return chat_by_room.get(room); 61 | } 62 | 63 | public NotificationCompat.MessagingStyle addChatRoomMessagesToStyle(NotificationCompat.MessagingStyle style, 64 | String room){ 65 | Chat chat = chat_by_room.get(room); 66 | if(chat == null){ 67 | Log.wtf(TAG, "Requested non-existent room or null is in chat_by_room map"); 68 | return null; 69 | } 70 | synchronized (chat.messages) { 71 | List cMessages = chat.messages; 72 | Collections.sort(cMessages); 73 | for (ChatMessage message : cMessages) { 74 | style = style.addMessage(message.text, message.timestamp, message.person); 75 | } 76 | } 77 | return style; 78 | } 79 | 80 | public Chat getChatByNotificationId(Integer notification_id){ 81 | return chat_by_notification_id.get(notification_id); 82 | } 83 | 84 | public void removeChat(@NonNull Chat chat){ 85 | synchronized (chat_by_room){ 86 | chat_by_room.remove(chat.room); 87 | } 88 | synchronized (chat_by_notification_id){ 89 | for(ChatMessage message : chat.messages){ 90 | chat_by_notification_id.remove(message.notification_id); 91 | } 92 | } 93 | synchronized (notification_id_by_room){ 94 | notification_id_by_room.remove(chat.room); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Notification/Processors/spreed/chat/ChatMessage.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Notification.Processors.spreed.chat; 2 | 3 | import androidx.core.app.Person; 4 | 5 | public class ChatMessage implements Comparable { 6 | public String text; 7 | public long timestamp; 8 | public int notification_id; 9 | public Person person; 10 | 11 | public ChatMessage(String text, Person person, long timestamp, int notification_id) { 12 | this.text = text; 13 | this.timestamp = timestamp; 14 | this.notification_id = notification_id; 15 | this.person = person; 16 | } 17 | 18 | @Override 19 | public int compareTo(ChatMessage o) { 20 | return (int)(this.timestamp - o.timestamp); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/ConnectionController.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.IntentFilter; 7 | import android.net.ConnectivityManager; 8 | import android.net.NetworkInfo; 9 | 10 | import com.polar.nextcloudservices.Services.Settings.ServiceSettings; 11 | import com.polar.nextcloudservices.Services.Status.Status; 12 | import com.polar.nextcloudservices.Services.Status.StatusCheckable; 13 | 14 | /** 15 | * Checks connectivity to the network 16 | */ 17 | public class ConnectionController implements StatusCheckable { 18 | private final ServiceSettings mServiceSettings; 19 | private final static String TAG = "Services.ConnectionController"; 20 | private BroadcastReceiver broadcastReceiver = null; 21 | public ConnectionController(ServiceSettings settings){ 22 | mServiceSettings = settings; 23 | } 24 | 25 | public boolean checkConnection(Context context) { 26 | ConnectivityManager connectivity = 27 | (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 28 | if (connectivity != null) { 29 | //We need to check only active network state 30 | final NetworkInfo activeNetwork = connectivity.getActiveNetworkInfo(); 31 | if (activeNetwork != null) { 32 | if (activeNetwork.isConnected()) { 33 | if (activeNetwork.isRoaming()) { 34 | //Log.d(TAG, "Network is in roaming"); 35 | return mServiceSettings.isRoamingConnectionAllowed(); 36 | } else if (connectivity.isActiveNetworkMetered()) { 37 | //Log.d(TAG, "Network is metered"); 38 | return mServiceSettings.isMeteredConnectionAllowed(); 39 | } else { 40 | //Log.d(TAG, "Network is unmetered"); 41 | return true; 42 | } 43 | } 44 | } 45 | } 46 | return false; 47 | } 48 | 49 | @Override 50 | public Status getStatus(Context context) { 51 | if(checkConnection(context)){ 52 | return Status.Ok(); 53 | } 54 | return Status.Failed("Disconnected: no suitable network found."); 55 | } 56 | 57 | public void setConnectionStatusListener(Context context, IConnectionStatusListener listener){ 58 | if(broadcastReceiver != null){ 59 | context.unregisterReceiver(broadcastReceiver); 60 | } 61 | broadcastReceiver = new BroadcastReceiver() { 62 | public void onReceive(Context context, Intent intent) { 63 | listener.onConnectionStatusChanged(checkConnection(context)); 64 | }}; 65 | context.registerReceiver(broadcastReceiver, 66 | new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE")); 67 | } 68 | 69 | public void removeConnectionListener(Context context){ 70 | context.unregisterReceiver(broadcastReceiver); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/IConnectionStatusListener.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services; 2 | 3 | public interface IConnectionStatusListener { 4 | void onConnectionStatusChanged(boolean isConnected); 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/INotificationListener.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services; 2 | 3 | import org.json.JSONObject; 4 | 5 | /** 6 | * An interface for delivering new notifications 7 | */ 8 | public interface INotificationListener { 9 | void onNewNotifications(JSONObject response); 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/INotificationService.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services; 2 | 3 | /** 4 | * Interface for communicating with notification service from side of UI 5 | */ 6 | public interface INotificationService { 7 | String getStatus(); 8 | void onPreferencesChanged(); 9 | void onAccountChanged(); 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/NotificationPollService.java: -------------------------------------------------------------------------------- 1 | 2 | package com.polar.nextcloudservices.Services; 3 | 4 | import android.app.Service; 5 | import android.content.Intent; 6 | import android.os.AsyncTask; 7 | import android.os.Handler; 8 | import android.os.IBinder; 9 | import android.util.Log; 10 | 11 | 12 | import org.json.JSONObject; 13 | 14 | import java.util.Timer; 15 | import java.util.TimerTask; 16 | 17 | 18 | import com.polar.nextcloudservices.API.INextcloudAbstractAPI; 19 | import com.polar.nextcloudservices.Notification.NotificationController; 20 | import com.polar.nextcloudservices.Services.Settings.ServiceSettings; 21 | import com.polar.nextcloudservices.Services.Status.StatusController; 22 | 23 | class PollTask extends AsyncTask { 24 | private static final String TAG = "Services.NotificationPollService.PollTask"; 25 | @Override 26 | protected JSONObject doInBackground(NotificationPollService... services) { 27 | Log.d(TAG, "Checking notifications"); 28 | INextcloudAbstractAPI api = services[0].getAPI(); 29 | try { 30 | boolean hasNotifications = api.checkNewNotifications(); 31 | if(hasNotifications) { 32 | return api.getNotifications(services[0]); 33 | } 34 | } catch (Exception e) { 35 | Log.e(TAG, "Can not check new notifications"); 36 | e.printStackTrace(); 37 | } 38 | return null; 39 | } 40 | } 41 | 42 | public class NotificationPollService extends Service 43 | implements INotificationListener, INotificationService { 44 | // constant 45 | public Integer pollingInterval = null; 46 | public static final String TAG = "Services.NotificationPollService"; 47 | private NotificationServiceBinder mBinder; 48 | private PollTimerTask task; 49 | public INextcloudAbstractAPI mAPI; 50 | private ServiceSettings mServiceSettings; 51 | private ConnectionController mConnectionController; 52 | private StatusController mStatusController; 53 | private NotificationController mNotificationController; 54 | // run on another Thread to avoid crash 55 | private final Handler mHandler = new Handler(); 56 | // timer handling 57 | private Timer mTimer = null; 58 | 59 | @Override 60 | public String getStatus() { 61 | return mStatusController.getStatusString(); 62 | } 63 | 64 | 65 | public void onNewNotifications(JSONObject response) { 66 | if(response != null) { 67 | mNotificationController.onNotificationsUpdated(response); 68 | } 69 | } 70 | 71 | @Override 72 | public IBinder onBind(Intent intent) { 73 | return mBinder; 74 | } 75 | 76 | public void updateTimer() { 77 | task.cancel(); 78 | mTimer.purge(); 79 | mTimer = new Timer(); 80 | task = new PollTimerTask(); 81 | mTimer.scheduleAtFixedRate(task, 0, pollingInterval); 82 | } 83 | 84 | private void startTimer(){ 85 | // cancel if already existed 86 | if (mTimer != null) { 87 | mTimer.cancel(); 88 | } else { 89 | // recreate new 90 | mTimer = new Timer(); 91 | } 92 | // schedule task 93 | task = new PollTimerTask(); 94 | mTimer.scheduleAtFixedRate(task, 0, pollingInterval); 95 | } 96 | 97 | public INextcloudAbstractAPI getAPI(){ 98 | if(mServiceSettings == null){ 99 | Log.wtf(TAG, "mServiceSettings is null!"); 100 | return null; 101 | } 102 | return mServiceSettings.getAPIFromSettings(); 103 | } 104 | 105 | @Override 106 | public void onCreate() { 107 | mBinder = new NotificationServiceBinder(this); 108 | mServiceSettings = new ServiceSettings(this); 109 | mAPI = mServiceSettings.getAPIFromSettings(); 110 | pollingInterval = mServiceSettings.getPollingIntervalMs(); 111 | Log.d(TAG, "onCreate: Set polling interval to " + pollingInterval); 112 | mConnectionController = new ConnectionController(mServiceSettings); 113 | mNotificationController = new NotificationController(this, mServiceSettings); 114 | mStatusController = new StatusController(this); 115 | mStatusController.addComponent(NotificationServiceComponents.SERVICE_COMPONENT_CONNECTION, 116 | mConnectionController, NotificationServiceConfig.CONNECTION_COMPONENT_PRIORITY); 117 | mStatusController.addComponent( 118 | NotificationServiceComponents.SERVICE_COMPONENT_NOTIFICATION_CONTROLLER, 119 | mNotificationController, 120 | NotificationServiceConfig.NOTIFICATION_CONTROLLER_PRIORITY); 121 | mStatusController.addComponent(NotificationServiceComponents.SERVICE_COMPONENT_API, 122 | mAPI, NotificationServiceConfig.API_COMPONENT_PRIORITY); 123 | startTimer(); 124 | startForeground(1, mNotificationController.getServiceNotification()); 125 | } 126 | 127 | @Override 128 | public void onPreferencesChanged() { 129 | mServiceSettings.onPreferencesChanged(); 130 | int _pollingInterval = mServiceSettings.getPollingIntervalMs(); 131 | onAccountChanged(); 132 | if (_pollingInterval != pollingInterval) { 133 | Log.d(TAG, "Updating timer"); 134 | pollingInterval = _pollingInterval; 135 | updateTimer(); 136 | } 137 | 138 | } 139 | 140 | @Override 141 | public void onDestroy() { 142 | Log.i(TAG, "Destroying service"); 143 | task.cancel(); 144 | mTimer.purge(); 145 | } 146 | 147 | 148 | 149 | 150 | @Override 151 | public void onAccountChanged(){ 152 | mAPI = mServiceSettings.getAPIFromSettings(); 153 | mStatusController.addComponent(NotificationServiceComponents.SERVICE_COMPONENT_API, mAPI, 154 | NotificationServiceConfig.CONNECTION_COMPONENT_PRIORITY); 155 | } 156 | 157 | class PollTimerTask extends TimerTask { 158 | 159 | @Override 160 | public void run() { 161 | // run on another thread 162 | mHandler.post(() -> { 163 | if (mConnectionController.checkConnection(getApplicationContext())) { 164 | new PollTask().execute(NotificationPollService.this); 165 | } 166 | }); 167 | } 168 | 169 | } 170 | } -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/NotificationServiceBinder.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services; 2 | 3 | public class NotificationServiceBinder extends android.os.Binder { 4 | private final INotificationService mNotificationService; 5 | public NotificationServiceBinder(INotificationService service){ 6 | super(); 7 | mNotificationService = service; 8 | } 9 | // Returns current status string of a service 10 | public String getServiceStatus() { 11 | return mNotificationService.getStatus(); 12 | } 13 | 14 | // Runs re-check of preferences, can be called from activities 15 | public void onPreferencesChanged() { 16 | mNotificationService.onPreferencesChanged(); 17 | } 18 | 19 | // Update API class when accounts state change 20 | public void onAccountChanged() { 21 | mNotificationService.onAccountChanged(); 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/NotificationServiceComponents.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services; 2 | 3 | public class NotificationServiceComponents { 4 | public static final Integer SERVICE_COMPONENT_API = 0x1; 5 | public static final Integer SERVICE_COMPONENT_CONNECTION = 0x2; 6 | public static final Integer SERVICE_COMPONENT_NOTIFICATION_CONTROLLER = 0x3; 7 | public static final Integer SERVICE_COMPONENT_WEBSOCKET = 0x4; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/NotificationServiceConfig.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services; 2 | 3 | public class NotificationServiceConfig { 4 | public static final Integer API_COMPONENT_PRIORITY = 2; 5 | public static final Integer CONNECTION_COMPONENT_PRIORITY = 3; 6 | public static final Integer NOTIFICATION_CONTROLLER_PRIORITY = 1; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/NotificationServiceController.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.ServiceConnection; 6 | import android.os.Build; 7 | import android.util.Log; 8 | 9 | import com.polar.nextcloudservices.Services.Settings.ServiceSettings; 10 | 11 | /** 12 | * Implements logic of choosing between services 13 | */ 14 | public class NotificationServiceController { 15 | private final ServiceSettings mServiceSettings; 16 | private Class lastServiceClass; 17 | private static final String TAG = "Services.NotificationServiceController"; 18 | 19 | public NotificationServiceController(ServiceSettings serviceSettings){ 20 | mServiceSettings = serviceSettings; 21 | lastServiceClass = getServiceClass(); 22 | } 23 | 24 | public Class getServiceClass(){ 25 | if(mServiceSettings.isWebsocketEnabled()){ 26 | return NotificationWebsocketService.class; 27 | }else{ 28 | return NotificationPollService.class; 29 | } 30 | } 31 | 32 | public void startService(Context context){ 33 | Class serviceClass = getServiceClass(); 34 | Log.i(TAG, "Starting service..."); 35 | Log.d(TAG, "Class: " + serviceClass); 36 | Intent intent = new Intent(context, serviceClass); 37 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 38 | context.startForegroundService(intent); 39 | } else { 40 | context.startService(intent); 41 | } 42 | } 43 | 44 | public void stopService(Context context){ 45 | Log.i(TAG, "Stopping service..."); 46 | Class serviceClass = getServiceClass(); 47 | Log.d(TAG, "Class: " + serviceClass); 48 | if(serviceClass == null){ 49 | Log.w(TAG, "Can not stop service: we do not know its class"); 50 | return; 51 | } 52 | context.stopService(new Intent(context, serviceClass)); 53 | } 54 | 55 | public void onServiceClassChange(Context context){ 56 | Log.d(TAG, "onServiceClassChange: old class = " + lastServiceClass); 57 | Class serviceClass = getServiceClass(); 58 | Log.d(TAG, "onServiceClassChange: new class = " + serviceClass); 59 | if(lastServiceClass == serviceClass){ 60 | Log.w(TAG, "Service class is unchanged, doing nothing"); 61 | } 62 | //Stop old service 63 | Log.i(TAG, "Stopping service..."); 64 | Log.d(TAG, "Class: " + lastServiceClass); 65 | context.stopService(new Intent(context, lastServiceClass)); 66 | lastServiceClass = serviceClass; 67 | //Start new service 68 | startService(context); 69 | } 70 | 71 | public void bindService(Context context, ServiceConnection connection){ 72 | Log.d(TAG, "binding service"); 73 | Class serviceClass = getServiceClass(); 74 | context.bindService(new Intent(context, serviceClass), 75 | connection, 0); 76 | } 77 | 78 | public void restartService(Context context){ 79 | Log.i(TAG, "Restarting service"); 80 | stopService(context); 81 | startService(context); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/NotificationWebsocketService.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services; 2 | 3 | import android.app.Notification; 4 | import android.app.Service; 5 | import android.content.Intent; 6 | import android.os.IBinder; 7 | import android.util.Log; 8 | 9 | import com.polar.nextcloudservices.API.INextcloudAbstractAPI; 10 | import com.polar.nextcloudservices.API.websocket.NotificationWebsocket; 11 | import com.polar.nextcloudservices.API.websocket.INotificationWebsocketEventListener; 12 | import com.polar.nextcloudservices.Notification.NotificationController; 13 | import com.polar.nextcloudservices.Services.Settings.ServiceSettings; 14 | import com.polar.nextcloudservices.Services.Status.StatusController; 15 | import com.polar.nextcloudservices.Utils.CommonUtil; 16 | 17 | import org.json.JSONObject; 18 | 19 | public class NotificationWebsocketService extends Service 20 | implements INotificationWebsocketEventListener, 21 | IConnectionStatusListener, INotificationService{ 22 | private INextcloudAbstractAPI mAPI; 23 | private ServiceSettings mServiceSettings; 24 | private NotificationController mNotificationController; 25 | private ConnectionController mConnectionController; 26 | private StatusController mStatusController; 27 | private NotificationWebsocket mNotificationWebsocket; 28 | private NotificationServiceBinder mBinder; 29 | private Thread mWsThread; 30 | private final static String TAG = "Services.NotificationWebsocketService"; 31 | 32 | @Override 33 | public void onCreate(){ 34 | Log.d(TAG, "onCreate"); 35 | mServiceSettings = new ServiceSettings(this); 36 | mNotificationController = new NotificationController(this, mServiceSettings); 37 | mAPI = mServiceSettings.getAPIFromSettings(); 38 | mStatusController = new StatusController(this); 39 | mConnectionController = new ConnectionController(mServiceSettings); 40 | mStatusController.addComponent(NotificationServiceComponents.SERVICE_COMPONENT_CONNECTION, 41 | mNotificationController, 42 | NotificationServiceConfig.NOTIFICATION_CONTROLLER_PRIORITY); 43 | mStatusController.addComponent(NotificationServiceComponents.SERVICE_COMPONENT_CONNECTION, 44 | mConnectionController, NotificationServiceConfig.CONNECTION_COMPONENT_PRIORITY); 45 | mConnectionController.setConnectionStatusListener(this, this); 46 | mWsThread = new Thread(this::listenForever); 47 | mWsThread.start(); 48 | Notification notification = mNotificationController.getServiceNotification(); 49 | startForeground(2, notification); 50 | Log.d(TAG, "startForeground done"); 51 | mBinder = new NotificationServiceBinder(this); 52 | } 53 | 54 | @Override 55 | public IBinder onBind(Intent intent) { 56 | Log.d(TAG, "onBind"); 57 | if(mBinder == null){ 58 | Log.e(TAG, "Binder is null!"); 59 | } 60 | return mBinder; 61 | } 62 | 63 | @Override 64 | public void onNewNotifications(JSONObject response) { 65 | if(response != null){ 66 | mNotificationController.onNotificationsUpdated(response); 67 | }else{ 68 | Log.e(TAG, "null response for notifications"); 69 | } 70 | } 71 | 72 | @Override 73 | public void onConnectionStatusChanged(boolean isConnected){ 74 | if(mNotificationWebsocket == null){ 75 | Log.w(TAG, "Notification websocket is currently null. Ignoring connection state change."); 76 | return; 77 | } 78 | if(!mNotificationWebsocket.getConnected() && isConnected){ 79 | Log.d(TAG, "Not connected: restarting connection"); 80 | mWsThread.interrupt(); 81 | mWsThread = new Thread(this::listenForever); 82 | mWsThread.start(); 83 | } 84 | } 85 | 86 | private boolean startListening(){ 87 | try { 88 | mNotificationWebsocket = mAPI.getNotificationsWebsocket(this); 89 | mStatusController.addComponent( 90 | NotificationServiceComponents.SERVICE_COMPONENT_WEBSOCKET, 91 | mNotificationWebsocket, NotificationServiceConfig.API_COMPONENT_PRIORITY); 92 | mAPI.getNotifications(this); 93 | return true; 94 | } catch (Exception e) { 95 | Log.e(TAG, "Exception while starting listening:", e); 96 | } 97 | return false; 98 | } 99 | 100 | private void listenForever(){ 101 | for(;;){ 102 | if(mConnectionController.checkConnection(this)){ 103 | if(startListening()){ 104 | return; 105 | } 106 | } else { 107 | Log.w(TAG, "We are not connected, not starting-up"); 108 | } 109 | Log.i(TAG, "Restart pause 3s"); 110 | CommonUtil.safeSleep(3000); 111 | } 112 | } 113 | 114 | /** 115 | * @param isError whether disconnect resulted from error 116 | */ 117 | @Override 118 | public void onWebsocketDisconnected(boolean isError) { 119 | if(mConnectionController.checkConnection(this)){ 120 | Log.w(TAG, "Received disconnect from websocket. Restart pause 3 seconds"); 121 | CommonUtil.safeSleep(3000); 122 | startListening(); 123 | } else { 124 | Log.w(TAG, "Disconnected from websocket. Seems that we have no network"); 125 | //listenForever(); 126 | } 127 | } 128 | 129 | @Override 130 | public void onWebsocketConnected() { 131 | /* stub */ 132 | } 133 | 134 | @Override 135 | public String getStatus() { 136 | if(!mConnectionController.checkConnection(this)){ 137 | return "Disconnected: no suitable network found"; 138 | }else if(mNotificationWebsocket == null){ 139 | return "Disconnected: can not connect to websocket. Are login details correct? Is notify_push installed on server?"; 140 | } 141 | return mStatusController.getStatusString(); 142 | } 143 | 144 | private void safeCloseWebsocket(){ 145 | if(mNotificationWebsocket != null){ 146 | mNotificationWebsocket.close(); 147 | } 148 | } 149 | 150 | @Override 151 | public void onPreferencesChanged() { 152 | safeCloseWebsocket(); 153 | if(!mServiceSettings.isWebsocketEnabled()){ 154 | Log.i(TAG, "Websocket is no more enabled. Disconnecting websocket and stopping service"); 155 | stopForeground(true); 156 | }else{ 157 | Log.i(TAG, "Preferences changed. Re-connecting to websocket."); 158 | mAPI = mServiceSettings.getAPIFromSettings(); 159 | } 160 | } 161 | 162 | @Override 163 | public void onAccountChanged() { 164 | Log.i(TAG, "Account changed. Re-connecting to websocket."); 165 | mNotificationWebsocket.close(); 166 | mAPI = mServiceSettings.getAPIFromSettings(); 167 | startListening(); 168 | } 169 | 170 | @Override 171 | public void onDestroy(){ 172 | mConnectionController.removeConnectionListener(this); 173 | super.onDestroy(); 174 | } 175 | } -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/Settings/ServiceSettingConfig.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services.Settings; 2 | 3 | public class ServiceSettingConfig { 4 | public static final String ALLOW_METERED = "allow_metered"; 5 | public static final String ALLOW_ROAMING = "allow_roaming"; 6 | public static final String REMOVE_ON_DISMISS = "remove_on_dismiss"; 7 | public static final String POLLING_INTERVAL = "polling_interval"; 8 | public static final String USERNAME = "login"; 9 | public static final String PASSWORD = "password"; 10 | public static final String SERVER = "server"; 11 | public static final String USE_HTTP = "insecure_connection"; 12 | public static final String OPEN_SPREED_IN_BROWSER = "open_spreed_in_browser"; 13 | public static final String USE_WEBSOCKET = "use_websocket"; 14 | public static final String ENABLE_SERVICE = "enable_polling"; 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/Settings/ServiceSettings.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services.Settings; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.util.Log; 6 | 7 | import androidx.preference.PreferenceManager; 8 | 9 | import com.nextcloud.android.sso.model.SingleSignOnAccount; 10 | import com.polar.nextcloudservices.API.INextcloudAbstractAPI; 11 | import com.polar.nextcloudservices.API.NextcloudHttpAPI; 12 | import com.polar.nextcloudservices.API.NextcloudSSOAPI; 13 | 14 | /** 15 | * Implements interface for accessing settings 16 | */ 17 | public class ServiceSettings { 18 | private static final String TAG = "Services.Settings.ServiceSettings"; 19 | private final Context mContext; 20 | private INextcloudAbstractAPI mCachedAPI = null; 21 | 22 | public ServiceSettings(Context context){ 23 | mContext = context; 24 | } 25 | 26 | private INextcloudAbstractAPI makeAPIFromSettings(){ 27 | if (getBoolPreference("sso_enabled", false)) { 28 | final String name = getPreference("sso_name"); 29 | final String server = getPreference("sso_server"); 30 | final String type = getPreference("sso_type"); 31 | final String token = getPreference("sso_token"); 32 | final String userId = getPreference("sso_userid"); 33 | final SingleSignOnAccount ssoAccount = new SingleSignOnAccount(name, userId, token, server, type); 34 | return new NextcloudSSOAPI(mContext, ssoAccount); 35 | } else { 36 | //We do not have an account -> use HTTP API 37 | Log.i(TAG, "No Nextcloud account was found."); 38 | return new NextcloudHttpAPI(this); 39 | } 40 | } 41 | 42 | public INextcloudAbstractAPI getAPIFromSettings(){ 43 | if(mCachedAPI == null){ 44 | mCachedAPI = makeAPIFromSettings(); 45 | } 46 | return mCachedAPI; 47 | } 48 | 49 | public void onPreferencesChanged(){ 50 | mCachedAPI = makeAPIFromSettings(); 51 | } 52 | 53 | public String getPreference(String key) { 54 | SharedPreferences sharedPreferences = 55 | PreferenceManager.getDefaultSharedPreferences(mContext); 56 | return sharedPreferences.getString(key, ""); 57 | } 58 | 59 | public Integer getIntPreference(String key, int i) { 60 | SharedPreferences sharedPreferences = 61 | PreferenceManager.getDefaultSharedPreferences(mContext); 62 | return sharedPreferences.getInt(key, Integer.MIN_VALUE); 63 | } 64 | 65 | public boolean getBoolPreference(String key, boolean fallback) { 66 | SharedPreferences sharedPreferences = 67 | PreferenceManager.getDefaultSharedPreferences(mContext); 68 | return sharedPreferences.getBoolean(key, fallback); 69 | } 70 | 71 | public boolean isMeteredConnectionAllowed(){ 72 | return getBoolPreference(ServiceSettingConfig.ALLOW_METERED, false); 73 | } 74 | 75 | public boolean isRoamingConnectionAllowed(){ 76 | return getBoolPreference(ServiceSettingConfig.ALLOW_ROAMING, false); 77 | } 78 | 79 | public boolean isRemoveOnDismissEnabled() { 80 | return getBoolPreference(ServiceSettingConfig.REMOVE_ON_DISMISS, false); 81 | } 82 | 83 | public int getPollingIntervalMs() { 84 | return getIntPreference(ServiceSettingConfig.POLLING_INTERVAL, 10) * 1000; 85 | } 86 | 87 | public String getUsername() { 88 | return getPreference(ServiceSettingConfig.USERNAME); 89 | } 90 | 91 | public String getPassword() { 92 | return getPreference(ServiceSettingConfig.PASSWORD); 93 | } 94 | 95 | public String getServer() { 96 | return getPreference(ServiceSettingConfig.SERVER); 97 | } 98 | 99 | public boolean getUseHttp(){ 100 | return getBoolPreference(ServiceSettingConfig.USE_HTTP, false); 101 | } 102 | 103 | public boolean getSpreedOpenedInBrowser(){ 104 | return getBoolPreference(ServiceSettingConfig.OPEN_SPREED_IN_BROWSER, true); 105 | } 106 | 107 | public boolean isWebsocketEnabled(){ 108 | return getBoolPreference(ServiceSettingConfig.USE_WEBSOCKET, false); 109 | } 110 | 111 | public boolean isServiceEnabled(){ 112 | return getBoolPreference(ServiceSettingConfig.ENABLE_SERVICE, true); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/Status/Status.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services.Status; 2 | 3 | /* 4 | * Represents status of polling. 5 | * Stores boolean indicating whether service connecting and working 6 | * and a string reason why service has fault. 7 | */ 8 | public class Status { 9 | public String reason; 10 | public boolean isOk; 11 | 12 | public Status(boolean _isOk, String _reason){ 13 | isOk = _isOk; 14 | reason = _reason; 15 | } 16 | 17 | public static Status Ok(){ 18 | return new Status(true, null); 19 | } 20 | 21 | public static Status Failed(String reason){ 22 | return new Status(false, reason); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/Status/StatusCheckable.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services.Status; 2 | 3 | import android.content.Context; 4 | 5 | public interface StatusCheckable { 6 | /** 7 | * @param context context which may be used for obtaining app and device status 8 | * @return status information in form of Status class 9 | */ 10 | Status getStatus(Context context); 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Services/Status/StatusController.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Services.Status; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import java.lang.reflect.Array; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.Vector; 12 | 13 | /* 14 | * Implements status-checking logic 15 | */ 16 | public class StatusController { 17 | private static final String TAG = "Services.Status.StatusController"; 18 | private final HashMap components; 19 | private final HashMap components_priority_mapping; 20 | private final Context mContext; 21 | 22 | public StatusController(Context context){ 23 | components = new HashMap<>(); 24 | mContext = context; 25 | components_priority_mapping = new HashMap<>(); 26 | } 27 | 28 | public void addComponent(@NonNull Integer componentId, @NonNull StatusCheckable component, 29 | @NonNull Integer priority){ 30 | components.put(componentId, component); 31 | components_priority_mapping.put(componentId, priority); 32 | } 33 | 34 | public void removeComponent(Integer componentId){ 35 | components.remove(componentId); 36 | components_priority_mapping.remove(componentId); 37 | } 38 | 39 | public Status check(){ 40 | Integer maxPriority = Integer.MIN_VALUE; 41 | Status status = Status.Ok(); 42 | for(Integer componentId: components.keySet()){ 43 | StatusCheckable component = components.get(componentId); 44 | if(component == null){ 45 | Log.e(TAG, "Can not get status for component with id: " + componentId); 46 | Log.e(TAG, "Component is null, skipping it"); 47 | continue; 48 | } 49 | Status state = component.getStatus(mContext); 50 | Integer priority = components_priority_mapping.getOrDefault(componentId, 51 | Integer.MIN_VALUE); 52 | if(!state.isOk){ 53 | Log.d(TAG, "Got status: " + state.reason); 54 | Log.d(TAG, "Component id: " + componentId); 55 | Log.d(TAG, "Component prio: " + priority); 56 | if(priority >= maxPriority){ 57 | status = state; 58 | maxPriority = priority; 59 | } 60 | } 61 | } 62 | return status; 63 | } 64 | 65 | public String getStatusString(){ 66 | Status status = check(); 67 | if(status.isOk){ 68 | return "Connected"; 69 | } else { 70 | return status.reason; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/polar/nextcloudservices/Utils/CommonUtil.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices.Utils; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.pm.PackageManager; 6 | import android.util.Log; 7 | 8 | import androidx.annotation.NonNull; 9 | 10 | import java.net.URI; 11 | import java.net.URISyntaxException; 12 | 13 | import java.text.SimpleDateFormat; 14 | import java.util.Arrays; 15 | import java.util.Date; 16 | 17 | 18 | public class CommonUtil { 19 | private static final String TAG = "Utils.CommonUtil"; 20 | 21 | 22 | /** 23 | * @param packageName name of package which should be checked 24 | * @param packageManager system package manager access object 25 | * @return true if package is present on device, false otherwise 26 | */ 27 | public static boolean isPackageInstalled(String packageName, PackageManager packageManager) { 28 | try { 29 | packageManager.getPackageInfo(packageName, 0); 30 | return true; 31 | } catch (PackageManager.NameNotFoundException e) { 32 | return false; 33 | } 34 | } 35 | 36 | /** 37 | * Clean-ups URL by removing domain and protocol if needed 38 | * according to wikipedia a uniform resource locator is composed of the following elements: 39 | * URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment] 40 | * authority = [userinfo "@"] host [":" port] 41 | * Example: "https://cloud.example.com/path?query#fragment" -> "/path?query#fragment" 42 | * @param target Target URL to remove everything in front of the path 43 | * @return String cleaned-up from protocol and domain 44 | */ 45 | public static String cleanUpURLIfNeeded(String target){ 46 | try { 47 | URI uri = new URI(target); 48 | String result = uri.getPath(); 49 | if(uri.getQuery() != null) { 50 | result = result + "?" + uri.getQuery(); 51 | } 52 | if(uri.getFragment() != null) { 53 | result = result + "#" + uri.getFragment(); 54 | } 55 | return result; 56 | } catch (URISyntaxException e) { 57 | Log.e(TAG, "error cleaning up target link."); 58 | e.printStackTrace(); 59 | return null; 60 | } 61 | } 62 | 63 | public static boolean isInArray(T obj, T[] array){ 64 | return Arrays.asList(array).contains(obj); 65 | } 66 | 67 | public static void safeSleep(long millis){ 68 | try { 69 | Thread.sleep(millis); 70 | } catch (InterruptedException e){ 71 | Log.e(TAG, "Interrupted while sleeping " + millis + "ms"); 72 | } 73 | } 74 | 75 | public static String cleanUpURLParams(@NonNull String chatroom){ 76 | String[] splits = chatroom.split("#"); 77 | if(splits.length == 0){ 78 | return null; 79 | } else { 80 | return splits[0]; 81 | } 82 | } 83 | 84 | public static long getTimestamp(){ 85 | return System.currentTimeMillis(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/app/src/main/res/drawable-v24/user.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_deck.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_icon_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_reply_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_credits.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/credits_contributer.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 28 | 29 | 40 | 41 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/settings_activity.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_o_s_s_licenses.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/booleans.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #1A1C1E 5 | #FFB4AB 6 | #93000A 7 | #2F3033 8 | #00639A 9 | #E2E2E5 10 | #E2E2E5 11 | #690005 12 | #FFB4AB 13 | #003353 14 | #CEE5FF 15 | #233240 16 | #D5E4F7 17 | #E2E2E5 18 | #C2C7CF 19 | #382A49 20 | #EEDBFF 21 | #8C9198 22 | #42474E 23 | #96CCFF 24 | #004A76 25 | #000000 26 | #B9C8DA 27 | #3A4857 28 | #000000 29 | #1A1C1E 30 | #42474E 31 | #D3BFE6 32 | #4F4061 33 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @string/reply_string 5 | @string/reply_to_all 6 | 7 | 8 | 9 | reply 10 | reply_all 11 | 12 | 13 | 14 | 15 | @string/contribution_invissvenska 16 | @string/contribution_Donnno 17 | @string/contribution_penguin86 18 | @string/contribution_Devansh_gaur_1611 19 | @string/contribution_freeflyk 20 | @string/contribution_stefan_niedermann 21 | 22 | 23 | 24 | https://avatars.githubusercontent.com/u/404166?v=4 25 | https://avatars.githubusercontent.com/u/31142286?v=4 26 | https://avatars.githubusercontent.com/u/6894017?v=4 27 | https://avatars.githubusercontent.com/u/89275109?v=4 28 | https://avatars.githubusercontent.com/u/65976525?v=4 29 | https://avatars.githubusercontent.com/u/4741199?v=4 30 | 31 | 32 | 33 | Sven van den Tweel 34 | Donno 35 | Daniele Verducci 36 | Devansh-gaur-1611 37 | freeflyk 38 | Niedermann IT-Dienstleistungen 39 | 40 | 41 | 42 | invissvenska 43 | Donnnno 44 | penguin86 45 | Devansh-gaur-1611 46 | freeflyk 47 | stefan-niedermann 48 | 49 | 50 | 51 | https://github.com/invissvenska/NumberPickerPreference/blob/master/LICENSE 52 | https://github.com/Donnnno 53 | https://github.com/penguin86 54 | https://github.com/Devansh-gaur-1611 55 | https://github.com/freeflyk 56 | https://github.com/stefan-niedermann 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/values/booleans.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #FCFCFF 5 | #BA1A1A 6 | #FFDAD6 7 | #F0F0F4 8 | #96CCFF 9 | #2F3033 10 | #1A1C1E 11 | #FFFFFF 12 | #410002 13 | #FFFFFF 14 | #001D32 15 | #FFFFFF 16 | #0E1D2A 17 | #1A1C1E 18 | #42474E 19 | #FFFFFF 20 | #231533 21 | #72777F 22 | #C2C7CF 23 | #00639A 24 | #CEE5FF 25 | #000000 26 | #51606F 27 | #D5E4F7 28 | #000000 29 | #FCFCFF 30 | #DEE3EB 31 | #68587A 32 | #EEDBFF 33 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 180dp 3 | 16dp 4 | 16dp 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Nextcloud Services 3 | Settings 4 | 5 | Reply 6 | 7 | Open Talk notifications in browser 8 | Enables opening Talk notifications via browser instead of launching Talk app 9 | 10 | Nextcloud Talk 11 | Two-factor authentication via notification 12 | App updates 13 | Thank you! ❤️ 14 | Quick action failed to complete 15 | Avatar 16 | Nextcloud address 17 | Address of your nextcloud server(without http(s)://) 18 | Password 19 | Your password 20 | Your username 21 | Login 22 | Log in via Nextcloud app 23 | Use on-device Nextcloud account 24 | Connection 25 | Use http instead of https 26 | Use insecure connection 27 | Allows checking for notifications when in roaming 28 | Allow roaming 29 | Allows networks which are metered 30 | Allow metered networks 31 | Enables polling notifications 32 | Enable service 33 | Use notify_push websocket for getting notifications 34 | Enable websocket 35 | Polling interval 36 | Remove notifications from server when they are dismissed. 37 | Remove notifications on dismiss 38 | Status 39 | Disconnected 40 | Others 41 | Credits 42 | See people who contributed to this project 43 | Donate 44 | Buy me a coffee 45 | Reply 46 | Reply to all 47 | Contribution: NumberPickerPreference\nLicense: LGPL-3.0 48 | Contribution: App icon 49 | Contribution: Bug fixes, ideas 50 | Contribution: Credits activity improvements 51 | Contribution: Nextcloud talk "Reply" button fix, other fixes and features 52 | Contribution: Redesign of app to Material You design, monochrome icon 53 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 36 | 37 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/xml/root_preferences.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 11 | 16 | 23 | 29 | 30 | 31 | 36 | 41 | 46 | 51 | 56 | 63 | 64 | 65 | 70 | 75 | 76 | 77 | 87 | 88 | 89 | 90 | 96 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /app/src/test/java/com/polar/nextcloudservices/CommonUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | import com.polar.nextcloudservices.Utils.CommonUtil; 8 | 9 | 10 | public class CommonUtilTest { 11 | @Test 12 | public void testURLCleanup(){ 13 | String result1 = CommonUtil.cleanUpURLIfNeeded("https://cloud.example.com/query?domain=cloud.example.com&path=https://cloud"); 14 | assertEquals(result1, "/query?domain=cloud.example.com&path=https://cloud"); 15 | String result3 = CommonUtil.cleanUpURLIfNeeded("https://cloud.example.com/query?domain=cloud.example.com&path=https://cloud"); 16 | assertEquals(result3, "/query?domain=cloud.example.com&path=https://cloud"); 17 | String result4 = CommonUtil.cleanUpURLIfNeeded("https://cloud.example.com:8080/query"); 18 | assertEquals(result4, "/query"); 19 | String result5 = CommonUtil.cleanUpURLIfNeeded("https://cloud.example.com:8080/query?domain=cloud.example.com&path=https://cloud"); 20 | assertEquals(result5, "/query?domain=cloud.example.com&path=https://cloud"); 21 | String result6 = CommonUtil.cleanUpURLIfNeeded("https://cloud.example.com:8080/query?domain=cloud.example.com&path=https://cloud#fragment"); 22 | assertEquals(result6, "/query?domain=cloud.example.com&path=https://cloud#fragment"); 23 | } 24 | 25 | @Test 26 | public void testInArray(){ 27 | final String[] items = {"foo"}; 28 | String foo = "foo"; 29 | String bar = "bar"; 30 | assertTrue(CommonUtil.isInArray(foo, items)); 31 | assertFalse(CommonUtil.isInArray(bar, items)); 32 | } 33 | 34 | @Test 35 | public void testCleanUpURLParams(){ 36 | assertEquals(CommonUtil.cleanUpURLParams(""), ""); 37 | assertEquals(CommonUtil.cleanUpURLParams("https://example.com/#abcdef"), 38 | "https://example.com/"); 39 | assertEquals(CommonUtil.cleanUpURLParams("https://example.com/url/example#abcdef"), 40 | "https://example.com/url/example"); 41 | assertEquals(CommonUtil.cleanUpURLParams( 42 | "https://example.com/url/example?a=b&c=d&f=42#abcdef"), 43 | "https://example.com/url/example?a=b&c=d&f=42"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/test/java/com/polar/nextcloudservices/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.polar.nextcloudservices; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:8.2.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | maven { url "https://jitpack.io" } 20 | } 21 | } 22 | 23 | task clean(type: Delete) { 24 | delete rootProject.buildDir 25 | } -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/10.txt: -------------------------------------------------------------------------------- 1 | - Updated default preferences to make app less battery-consuming 2 | - Added possibility to enable/disable service 3 | - Added forgotten changelog for beta9 4 | - Added icon(thanks to @Donnnno) 5 | - Added possibility to log in using Nextcloud SingleSignOn 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/11.txt: -------------------------------------------------------------------------------- 1 | * Added credits(thanks to @Devansh-gaur-1611) 2 | * Added app queries for Nextcloud SSO authentication 3 | * Now app opens browser when notification is clicked 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/12.txt: -------------------------------------------------------------------------------- 1 | - Now removed notifications can be synchronized with server 2 | - Talk notifications are now have messagings style and displayed in "Conversation" section on newer devices 3 | - Also fast "Reply" button was added for Talk notifications 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/13.txt: -------------------------------------------------------------------------------- 1 | - Fixed fast reply was not working(thanks to @freeflyk) 2 | - Improved S+ APIs compatability 3 | - Updated dependencies 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/14.txt: -------------------------------------------------------------------------------- 1 | - Image preview for Talk notifications(thanks to @freeflyk) 2 | - Fixed bug with a password not being masked(thanks to @freeflyk) 3 | - Open Talk chats in browser, improved handling of opening links(thanks to @freeflyk) 4 | - Handle custom actions in notifications(fixes 2FA via notification) 5 | - Android 13 notification permission request 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/15.txt: -------------------------------------------------------------------------------- 1 | - Allow to choose whether to open chats via Nextcloud Talk or via webbrowser 2 | - Update a notification when message to a certain chat received adding new messages in chat to a single notification instead of creating new notification per each message 3 | - Refactoring of a service 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/16.txt: -------------------------------------------------------------------------------- 1 | - Fix bug with non-updating update frequency 2 | - Update 2FA notification icon 3 | - Fix increased battery usage(probably) 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/17.txt: -------------------------------------------------------------------------------- 1 | - Monochrome icon(thanks to @stefan-niedermann) 2 | - Material-3 UI(thanks to @stefan-niedermann) 3 | - Optimized data usage(currently does not works with SSO login) 4 | - Minor fixes and improvements 5 | You may see issue about data usage optimization not working with SSO login by URL: https://github.com/Andrewerr/NextcloudServices/issues/68 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/18.txt: -------------------------------------------------------------------------------- 1 | - Put reply for Talk chats to notification 2 | - Fix quick reply error 3 | - Made strings translatable for future translations 4 | - Fix for Talk notifications groupping 5 | - Add websocket(experimental, SSO not supported yet) 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/19.txt: -------------------------------------------------------------------------------- 1 | - Fixed bugs associated with Android 14 SDK 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/20.txt: -------------------------------------------------------------------------------- 1 | - Fixes for Android 14 SDK(thanks to @freeflyk) 2 | - Fixes for websocket implementation 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | - Added prettifier for Nextcloud app names(from beta2 on GitHub) 2 | - Added Fastlane changelog 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/7.txt: -------------------------------------------------------------------------------- 1 | - Added status information to the app 2 | - Added connectivity check 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/8.txt: -------------------------------------------------------------------------------- 1 | - Fixed back arrow in main activity 2 | - Fixed nextcloud installations which are running under subdirectories(thanks @penguin86) 3 | - Added possibilty to use insecure connection 4 | - Added possibiliy to set polling interval 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/9.txt: -------------------------------------------------------------------------------- 1 | - Added possibility to disable polling in metered networks and roaming 2 | - Added possibility to donate 3 | - Fixed timer not being killed after activity exit 4 | 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Nextcloud services allow you to receive notifications from your Nextcloud services even if you do not have Google play services installed. 2 | 3 | INSTRUCTIONS: 4 | 5 | WITHOUT NEXTCLOUD APP: 6 | * At your Nextcloud open settings and navigate to "Security" 7 | * Generate per-app password 8 | * Enter you login and server address into the app(Enter server address without `https://` prefix) 9 | * Enter generated per-app password 10 | * On Nextcloud server click "Add" button to add generated password to list of authenticated devices(Additionally it is recommended to disable file access for this per-app password) 11 | 12 | IMPORTANT: Do **NOT** ommit first two steps - this may be risky for your security 13 | 14 | WITH NEXTCLOUD APP: 15 | * Click "Log-in via Nextcloud app" 16 | * Select account you want to use 17 | * In next dialog click "Allow button" 18 | 19 | CREDITS: 20 | * Nextcloud and ownCloud team for Nextcloud 21 | * Deck Android app for deck logo 22 | * Nextcloud app for Nextcloud logo and spreed(talk) logo 23 | * @penguin86 for fixing bugs and suggesting new idead 24 | * @Donno for creating app icon 25 | * @invissvenska for NumberPickerPreference(licensed under LGPL-3.0) 26 | * @Devansh-Gaur-1611 for creating credit activity in the app 27 | 28 | GITHUB REPOSITORY: https://github.com/Andrewerr/NextcloudServices 29 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Simply fetch Nextcloud notifications on devices without Google Play services 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | NextcloudServices 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | android.defaults.buildfeatures.buildconfig=true 21 | android.nonTransitiveRClass=false 22 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jan 13 17:48:36 EET 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /img/Screenshot_scaled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/img/Screenshot_scaled.png -------------------------------------------------------------------------------- /img/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xf104a/NextcloudServices/5adcafb4042a3a9e9a1374669a4034801dff919f/img/app_icon.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "Nextcloud Services" --------------------------------------------------------------------------------