├── .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 | [](https://github.com/Andrewerr/NextcloudServices/actions/workflows/gradle.yml)
3 | [](https://f-droid.org/wiki/page/com.polar.nextcloudservices/lastbuild)
4 | 
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 | 
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 | [](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 |
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------