Showing 46 changed files with 1463 additions and 0 deletions
+1
LibreNews-App/.gitignore
... ...
@@ -0,0 +1 @@
1
+/build
+35
LibreNews-App/build.gradle
... ...
@@ -0,0 +1,35 @@
1
+apply plugin: 'com.android.application'
2
+
3
+android {
4
+    compileSdkVersion 26
5
+    buildToolsVersion "26.0.0"
6
+    defaultConfig {
7
+        applicationId "app.librenews.io.librenews"
8
+        minSdkVersion 16
9
+        targetSdkVersion 26
10
+        versionCode 1
11
+        versionName "1.0"
12
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13
+        vectorDrawables.useSupportLibrary = true
14
+    }
15
+    buildTypes {
16
+        release {
17
+            minifyEnabled false
18
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
19
+        }
20
+    }
21
+}
22
+
23
+dependencies {
24
+    compile fileTree(dir: 'libs', include: ['*.jar'])
25
+    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
26
+        exclude group: 'com.android.support', module: 'support-annotations'
27
+    })
28
+    compile 'com.android.support:appcompat-v7:26.+'
29
+    compile 'com.android.support.constraint:constraint-layout:1.0.2'
30
+    compile 'org.ocpsoft.prettytime:prettytime:3.2.7.Final'
31
+    compile 'com.android.support:support-v4:26.+'
32
+    compile 'com.android.support:support-vector-drawable:26.+'
33
+    compile 'com.android.support:design:26.+'
34
+    testCompile 'junit:junit:4.12'
35
+}
+25
LibreNews-App/proguard-rules.pro
... ...
@@ -0,0 +1,25 @@
1
+# Add project specific ProGuard rules here.
2
+# By default, the flags in this file are appended to flags specified
3
+# in /Users/miles/Library/Android/sdk/tools/proguard/proguard-android.txt
4
+# You can edit the include path and order by changing the proguardFiles
5
+# directive in build.gradle.
6
+#
7
+# For more details, see
8
+#   http://developer.android.com/guide/developing/tools/proguard.html
9
+
10
+# Add any project specific keep options here:
11
+
12
+# If your project uses WebView with JS, uncomment the following
13
+# and specify the fully qualified class name to the JavaScript interface
14
+# class:
15
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16
+#   public *;
17
+#}
18
+
19
+# Uncomment this to preserve the line number information for
20
+# debugging stack traces.
21
+#-keepattributes SourceFile,LineNumberTable
22
+
23
+# If you keep the line number information, uncomment this to
24
+# hide the original source file name.
25
+#-renamesourcefileattribute SourceFile
+26
LibreNews-App/src/androidTest/java/app/librenews/io/librenews/ExampleInstrumentedTest.java
... ...
@@ -0,0 +1,26 @@
1
+package app.librenews.io.librenews;
2
+
3
+import android.content.Context;
4
+import android.support.test.InstrumentationRegistry;
5
+import android.support.test.runner.AndroidJUnit4;
6
+
7
+import org.junit.Test;
8
+import org.junit.runner.RunWith;
9
+
10
+import static org.junit.Assert.*;
11
+
12
+/**
13
+ * Instrumentation test, which will execute on an Android device.
14
+ *
15
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
16
+ */
17
+@RunWith(AndroidJUnit4.class)
18
+public class ExampleInstrumentedTest {
19
+    @Test
20
+    public void useAppContext() throws Exception {
21
+        // Context of the app under test.
22
+        Context appContext = InstrumentationRegistry.getTargetContext();
23
+
24
+        assertEquals("app.librenews.io.librenews", appContext.getPackageName());
25
+    }
26
+}
+36
LibreNews-App/src/main/AndroidManifest.xml
... ...
@@ -0,0 +1,36 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3
+    package="app.librenews.io.librenews">
4
+
5
+    <uses-permission android:name="android.permission.INTERNET" />
6
+
7
+    <application
8
+        android:allowBackup="true"
9
+        android:icon="@mipmap/ic_launcher"
10
+        android:label="@string/app_name"
11
+        android:roundIcon="@mipmap/ic_launcher_round"
12
+        android:supportsRtl="true"
13
+        android:theme="@style/AppTheme">
14
+        <activity android:name=".views.FlashView">
15
+            <intent-filter>
16
+                <action android:name="android.intent.action.MAIN" />
17
+
18
+                <category android:name="android.intent.category.LAUNCHER" />
19
+            </intent-filter>
20
+        </activity>
21
+
22
+        <receiver
23
+            android:name=".controllers.SyncManager$RefreshBroadcastReceiver"
24
+            android:exported="true">
25
+            <intent-filter>
26
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
27
+            </intent-filter>
28
+        </receiver>
29
+
30
+        <activity
31
+            android:name=".views.SettingsActivity"
32
+            android:label="@string/title_activity_settings"
33
+            android:theme="@style/AppTheme.NoActionBar"></activity>
34
+    </application>
35
+
36
+</manifest>
+33
LibreNews-App/src/main/java/app/librenews/io/librenews/controllers/DebugManager.java
... ...
@@ -0,0 +1,33 @@
1
+package app.librenews.io.librenews.controllers;
2
+
3
+import android.app.NotificationManager;
4
+import android.content.Context;
5
+import android.content.SharedPreferences;
6
+import android.net.Uri;
7
+import android.preference.PreferenceManager;
8
+import android.support.v4.app.NotificationCompat;
9
+
10
+import app.librenews.io.librenews.R;
11
+
12
+/**
13
+ * Created by miles on 7/15/17.
14
+ */
15
+
16
+public class DebugManager {
17
+    public static void sendDebugNotification(String message, Context context){
18
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
19
+        if(prefs.getBoolean("debug", false)){
20
+            NotificationCompat.Builder mBuilder =
21
+                    new NotificationCompat.Builder(context)
22
+                            .setSmallIcon(R.drawable.ic_debug)
23
+                            .setStyle(new NotificationCompat.BigTextStyle()
24
+                                    .bigText(message))
25
+                            .setContentText(message);
26
+            NotificationManager mNotificationManager =
27
+                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
28
+
29
+            mNotificationManager.notify(message.hashCode(), mBuilder.build());
30
+            System.out.println("LibreNews Debug: " + message);
31
+        }
32
+    }
33
+}
+228
LibreNews-App/src/main/java/app/librenews/io/librenews/controllers/FlashManager.java
... ...
@@ -0,0 +1,228 @@
1
+package app.librenews.io.librenews.controllers;
2
+
3
+import android.app.Activity;
4
+import android.app.AlarmManager;
5
+import android.app.NotificationManager;
6
+import android.app.PendingIntent;
7
+import android.content.Context;
8
+import android.content.Intent;
9
+import android.content.SharedPreferences;
10
+import android.net.Uri;
11
+import android.os.SystemClock;
12
+import android.preference.PreferenceManager;
13
+import android.support.v4.app.NotificationCompat;
14
+import android.support.v4.widget.SwipeRefreshLayout;
15
+import android.widget.Toast;
16
+
17
+import org.json.JSONArray;
18
+import org.json.JSONException;
19
+import org.json.JSONObject;
20
+
21
+import java.io.BufferedReader;
22
+import java.io.FileInputStream;
23
+import java.io.FileNotFoundException;
24
+import java.io.FileOutputStream;
25
+import java.io.IOException;
26
+import java.io.InputStreamReader;
27
+import java.net.MalformedURLException;
28
+import java.net.URL;
29
+import java.text.ParseException;
30
+import java.util.ArrayList;
31
+import java.util.Arrays;
32
+import java.util.Comparator;
33
+import java.util.List;
34
+
35
+import app.librenews.io.librenews.R;
36
+import app.librenews.io.librenews.models.Flash;
37
+
38
+/**
39
+ * Created by miles on 7/14/17.
40
+ */
41
+
42
+public class FlashManager {
43
+
44
+    int flashesToStoreInDatabase = 100;
45
+    final String flashFileLocation = "flashes.json";
46
+    SharedPreferences prefs;
47
+    String serverUrl;
48
+    String serverName;
49
+    Context context;
50
+
51
+    public FlashManager(Context context) {
52
+        this.context = context;
53
+        this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
54
+        this.serverUrl = prefs.getString("server_url", "https://librenews.io/api");
55
+        try {
56
+            loadFlashesFromStorage();
57
+        } catch (FileNotFoundException exception) {
58
+            try {
59
+                refresh();
60
+            } catch (Exception exception2) {
61
+                Toast.makeText(context.getApplicationContext(), context.getResources().getString(R.string.internal_storage_setup_fail), Toast.LENGTH_LONG);
62
+                exception2.printStackTrace();
63
+            }
64
+        } catch (Exception exception) {
65
+            Toast.makeText(context.getApplicationContext(), context.getResources().getString(R.string.internal_storage_read_fail), Toast.LENGTH_LONG);
66
+            exception.printStackTrace();
67
+        }
68
+
69
+        // first things first: get everything syncing!
70
+        SyncManager syncManager = new SyncManager(context, this);
71
+        syncManager.startSyncService();
72
+    }
73
+
74
+    public ArrayList<Flash> loadFlashesFromStorage() throws JSONException, IOException, ParseException {
75
+        FileInputStream inputStream = context.openFileInput(flashFileLocation);
76
+        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
77
+        StringBuilder builder = new StringBuilder();
78
+        ArrayList<Flash> latestPushedFlashes = new ArrayList<>();
79
+
80
+        String line;
81
+        while ((line = reader.readLine()) != null) {
82
+            builder.append(line);
83
+            builder.append('\n'); // you'd think Java would have a better way of handling this, but no!
84
+        }
85
+        reader.close();
86
+        inputStream.close();
87
+
88
+        // System.out.println(builder);
89
+
90
+        if(builder.toString().trim().equals("")){
91
+            throw new FileNotFoundException("No flash storage file exists or is empty.");
92
+        }
93
+
94
+        JSONArray jsonArray = new JSONArray(builder.toString());
95
+        for (int i = 0; i < jsonArray.length(); i++) {
96
+            latestPushedFlashes.add(Flash.deserialize((JSONObject) jsonArray.get(i)));
97
+        }
98
+        return latestPushedFlashes;
99
+    }
100
+
101
+    public void writeFlashesToStorage(List<Flash> flashes) throws JSONException, IOException {
102
+        FileOutputStream outputStream = context.openFileOutput(flashFileLocation, Context.MODE_PRIVATE);
103
+        String out = convertFlashesToOutputString(flashes);
104
+        outputStream.write(out.getBytes());
105
+        outputStream.close();
106
+    }
107
+
108
+    public void sortPushedFlashes(){
109
+        // todo
110
+    }
111
+
112
+    private String convertFlashesToOutputString(List<Flash> flashes) throws JSONException {
113
+        Flash[] sorted = flashes.toArray(new Flash[0]);
114
+        Arrays.sort(sorted, new Comparator<Flash>() {
115
+            public int compare(Flash a, Flash b){
116
+                return a.getDate().compareTo(b.getDate());
117
+            }
118
+        });
119
+
120
+        int min = 0;
121
+        int max = sorted.length - 1;
122
+        if(max < 0){
123
+            max = 0;
124
+        }
125
+        if (max > flashesToStoreInDatabase) {
126
+            min = max - flashesToStoreInDatabase;
127
+        }
128
+        JSONArray output = new JSONArray();
129
+        for (int i = min; i <= max; i++) {
130
+            output.put(sorted[i].serialize());
131
+        }
132
+        return output.toString(4);
133
+    }
134
+
135
+    public ArrayList<Flash> getLatestPushedFlashes() {
136
+        try {
137
+            return loadFlashesFromStorage();
138
+        }catch(Exception e){
139
+            DebugManager.sendDebugNotification("Unable to load flashes from storage: " + e.getLocalizedMessage(), context);
140
+            e.printStackTrace();
141
+        }
142
+        return new ArrayList<>();
143
+    }
144
+
145
+    public void clearPushedFlashes() throws JSONException, IOException{
146
+        writeFlashesToStorage(new ArrayList<Flash>());
147
+    }
148
+
149
+    public String getServerName() {
150
+        return serverName;
151
+    }
152
+
153
+    public void pushFlashNotification(Flash flash) throws JSONException, IOException {
154
+        if (!prefs.getBoolean("notifications_enabled", true)) {
155
+            return;
156
+        }
157
+        Intent notificationIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(flash.getLink()));
158
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
159
+        NotificationCompat.Builder mBuilder =
160
+                new NotificationCompat.Builder(context)
161
+                        .setSmallIcon(R.drawable.ic_alert)
162
+                        .setContentTitle(flash.getChannel() + " • " + flash.getSource())
163
+                        .setStyle(new NotificationCompat.BigTextStyle()
164
+                                .bigText(flash.getText()))
165
+                        .setContentText(flash.getText())
166
+                        .setSound(Uri.parse(prefs.getString("notification_sound", "DEFAULT")))
167
+                        .setContentIntent(pendingIntent);
168
+
169
+        NotificationManager mNotificationManager =
170
+                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
171
+
172
+        mNotificationManager.notify(flash.getIdAsInteger(), mBuilder.build());
173
+    }
174
+
175
+    public void refresh() {
176
+        String newServerUrl = prefs.getString("server_url", "https://librenews.io/api");
177
+        if (!newServerUrl.equals(serverUrl)) {
178
+            // they changed their server preferences!
179
+            try {
180
+                clearPushedFlashes();
181
+            } catch (IOException exception) {
182
+                Toast.makeText(context.getApplicationContext(), context.getResources().getString(R.string.internal_storage_setup_fail), Toast.LENGTH_LONG);
183
+                exception.printStackTrace();
184
+            } catch (JSONException exception) {
185
+                Toast.makeText(context.getApplicationContext(), context.getResources().getString(R.string.internal_storage_setup_fail), Toast.LENGTH_LONG);
186
+                exception.printStackTrace();
187
+            }
188
+        }
189
+        try {
190
+            FlashRetreiver retreiver = new FlashRetreiver(new URL(serverUrl));
191
+            retreiver.retrieveFlashes(new FlashRetreiver.FlashHandler() {
192
+                @Override
193
+                public void success(Flash[] flashes, String serverName) {
194
+                    for (Flash f : flashes){
195
+                        boolean pushed = false;
196
+                        for (Flash p : getLatestPushedFlashes()) {
197
+                            if (p.getId().equals(f.getId())) {
198
+                                pushed = true;
199
+                            }
200
+                        }
201
+                        try {
202
+                            if (!pushed) {
203
+                                pushFlashNotification(f);
204
+                                ArrayList<Flash> q = getLatestPushedFlashes();
205
+                                q.add(f);
206
+                                writeFlashesToStorage(q); // lots of IO, but it's OK
207
+                            }
208
+                        }catch(Exception exception){
209
+                            exception.printStackTrace();
210
+                            DebugManager.sendDebugNotification("Error occurred while trying push notifications: " + exception.getLocalizedMessage(), context);
211
+                        }
212
+                    }
213
+                }
214
+
215
+                @Override
216
+                public void failure(Exception exception) {
217
+                    exception.printStackTrace();
218
+                    DebugManager.sendDebugNotification("An error occurred while trying to receive flashes: " + exception.getLocalizedMessage(), context);
219
+                }
220
+            }, context);
221
+        } catch (MalformedURLException exception) {
222
+            Toast.makeText(context.getApplicationContext(), context.getResources().getString(R.string.invalid_server_url), Toast.LENGTH_LONG);
223
+            exception.printStackTrace();
224
+        } catch (Exception exception) {
225
+            exception.printStackTrace();
226
+        }
227
+    }
228
+}
+111
LibreNews-App/src/main/java/app/librenews/io/librenews/controllers/FlashRetreiver.java
... ...
@@ -0,0 +1,111 @@
1
+package app.librenews.io.librenews.controllers;
2
+
3
+import android.content.Context;
4
+import android.os.AsyncTask;
5
+import android.os.Debug;
6
+
7
+import org.json.JSONArray;
8
+import org.json.JSONException;
9
+import org.json.JSONObject;
10
+
11
+import java.io.BufferedReader;
12
+import java.io.IOException;
13
+import java.io.InputStreamReader;
14
+import java.net.HttpURLConnection;
15
+import java.net.URL;
16
+import java.text.ParseException;
17
+import java.util.ArrayList;
18
+
19
+import javax.net.ssl.HttpsURLConnection;
20
+
21
+import app.librenews.io.librenews.models.Flash;
22
+
23
+import static app.librenews.io.librenews.controllers.DebugManager.sendDebugNotification;
24
+
25
+public class FlashRetreiver {
26
+    interface FlashHandler {
27
+        void success(Flash[] flashes, String serverName);
28
+
29
+        void failure(Exception exception);
30
+    }
31
+
32
+    URL serverLocation;
33
+
34
+    public FlashRetreiver(URL serverLocation) {
35
+        this.serverLocation = serverLocation;
36
+    }
37
+
38
+
39
+    private Flash[] convertJsonToFlashes(JSONArray jsonFlashes) throws JSONException, ParseException {
40
+        ArrayList<Flash> flashes = new ArrayList<Flash>();
41
+        for (int i = 0; i < jsonFlashes.length(); i++) {
42
+            JSONObject jsonFlash = (JSONObject) jsonFlashes.get(i);
43
+            Flash flash = Flash.deserialize(jsonFlash);
44
+            flashes.add(flash);
45
+        }
46
+        return flashes.toArray(new Flash[0]);
47
+    }
48
+
49
+    Flash[] flashes = null;
50
+    String serverName = null;
51
+
52
+    private boolean retreiveFlashesNonAsync(Context context) throws IOException, JSONException, ParseException{
53
+        if (!serverLocation.getProtocol().equals("https")) {
54
+            throw new SecurityException("Flashes may only be retrieved over a secure HTTPS connection!");
55
+        }
56
+        sendDebugNotification("Retrieving flashes from " + serverLocation, context);
57
+        HttpsURLConnection urlConnection = (HttpsURLConnection) serverLocation.openConnection();
58
+        urlConnection.setRequestProperty("Content-Type", "application/json");
59
+        urlConnection.setRequestMethod("GET");
60
+        urlConnection.setReadTimeout(150000);
61
+        urlConnection.setConnectTimeout(15000);
62
+        urlConnection.connect();
63
+        if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
64
+            BufferedReader reader = new BufferedReader(new InputStreamReader(
65
+                    urlConnection.getInputStream()), 8);
66
+            StringBuilder sb = new StringBuilder();
67
+            String line = null;
68
+            while ((line = reader.readLine()) != null) {
69
+                sb.append(line + "\n");
70
+            }
71
+            String data = sb.toString();
72
+            JSONObject jsonData = new JSONObject(data);
73
+            serverName = jsonData.getString("server");
74
+            flashes = convertJsonToFlashes(jsonData.getJSONArray("latest"));
75
+        }
76
+        sendDebugNotification("Flashes found: " + flashes.length, context);
77
+        return true;
78
+    }
79
+
80
+    public void retrieveFlashes(final FlashHandler handler, final Context context) {
81
+        final URL serverLocation = this.serverLocation;
82
+        class JSONAsyncTask extends AsyncTask<String, Void, Boolean> {
83
+            @Override
84
+            protected void onPreExecute() {
85
+                super.onPreExecute();
86
+            }
87
+
88
+            @Override
89
+            protected Boolean doInBackground(String... params) {
90
+                try {
91
+                    return retreiveFlashesNonAsync(context);
92
+                }catch(Exception e){
93
+                    handler.failure(e);
94
+                    e.printStackTrace();
95
+                }
96
+                return false;
97
+            }
98
+
99
+            protected void onPostExecute(Boolean result) {
100
+                if (!result || flashes == null) {
101
+                    handler.failure(new Exception("An error occurred while trying to receive flashes!"));
102
+                    DebugManager.sendDebugNotification("An error occurred while trying to receive flashes!", context);
103
+                    return;
104
+                }
105
+                handler.success(flashes, serverName);
106
+            }
107
+        }
108
+        DebugManager.sendDebugNotification("Performing an asynchronous flash retrieval...", context);
109
+        new JSONAsyncTask().execute();
110
+    }
111
+}
+70
LibreNews-App/src/main/java/app/librenews/io/librenews/controllers/SyncManager.java
... ...
@@ -0,0 +1,70 @@
1
+package app.librenews.io.librenews.controllers;
2
+
3
+import android.app.Activity;
4
+import android.app.AlarmManager;
5
+import android.app.NotificationManager;
6
+import android.app.PendingIntent;
7
+import android.content.BroadcastReceiver;
8
+import android.content.Context;
9
+import android.content.Intent;
10
+import android.content.SharedPreferences;
11
+import android.os.SystemClock;
12
+import android.preference.PreferenceManager;
13
+import android.support.v4.app.NotificationCompat;
14
+
15
+import app.librenews.io.librenews.R;
16
+
17
+import static app.librenews.io.librenews.controllers.DebugManager.sendDebugNotification;
18
+
19
+/**
20
+ * Created by miles on 7/15/17.
21
+ */
22
+
23
+public class SyncManager {
24
+    Context context;
25
+    FlashManager flashManager;
26
+    SharedPreferences prefs;
27
+
28
+    public SyncManager(Context context, FlashManager flashManager) {
29
+        this.context = context;
30
+        this.flashManager = flashManager;
31
+        this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
32
+    }
33
+
34
+    public void startSyncService(){
35
+        Intent intent = new Intent(context, RefreshBroadcastReceiver.class);
36
+        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 11719, intent, 0);
37
+        String val = prefs.getString("refresh_preference", "60");
38
+        int syncRaw = Integer.valueOf(val);
39
+        long syncInterval = syncRaw*60000; // milliseconds
40
+        AlarmManager alarmMgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
41
+        alarmMgr.cancel(pendingIntent); // to ensure we don't have two sync daemons running
42
+        if(!prefs.getBoolean("automatically_refresh", true)){
43
+            sendDebugNotification("Sync service not enabled (would have been @ " + syncInterval + ")", context);
44
+            return;
45
+            // don't sync if they have disabled syncs!
46
+        }
47
+        alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
48
+                SystemClock.elapsedRealtime() + syncInterval,
49
+                syncInterval, pendingIntent);
50
+        sendDebugNotification("Sync service started at an interval of " + syncInterval, context);
51
+    }
52
+
53
+    public static class RefreshBroadcastReceiver extends BroadcastReceiver {
54
+        @Override
55
+        public void onReceive(Context context, Intent intent) {
56
+            sendDebugNotification("Syncing with server...", context);
57
+            FlashManager flashManager = new FlashManager(context);
58
+            flashManager.refresh();
59
+            sendDebugNotification("Sync completed!", context);
60
+        }
61
+    }
62
+
63
+    public static class BootReceiver extends BroadcastReceiver {
64
+        @Override
65
+        public void onReceive(Context context, Intent intent){
66
+            SyncManager manager = new SyncManager(context, new FlashManager(context));
67
+            manager.startSyncService();
68
+        }
69
+    }
70
+}
+119
LibreNews-App/src/main/java/app/librenews/io/librenews/models/Flash.java
... ...
@@ -0,0 +1,119 @@
1
+package app.librenews.io.librenews.models;
2
+
3
+import java.text.ParseException;
4
+import java.text.SimpleDateFormat;
5
+import java.util.Date;
6
+import java.util.Locale;
7
+
8
+import org.json.JSONException;
9
+import org.json.JSONObject;
10
+import org.ocpsoft.prettytime.PrettyTime;
11
+
12
+public class Flash {
13
+    String channel;
14
+    String id;
15
+    String link;
16
+    String source;
17
+    String text;
18
+    Date date;
19
+
20
+    public Flash(String channel, String id, String link, String source, String text, Date date) {
21
+        this.channel = channel;
22
+        this.id = id;
23
+        this.link = link;
24
+        this.source = source;
25
+        this.text = text;
26
+        this.date = date;
27
+    }
28
+
29
+    public String getChannel() {
30
+        return channel;
31
+    }
32
+
33
+    public void setChannel(String channel) {
34
+        this.channel = channel;
35
+    }
36
+
37
+    public int getIdAsInteger() {
38
+        try{
39
+            return Integer.parseInt(id);
40
+        }catch(NumberFormatException e){
41
+            return id.hashCode();
42
+        }
43
+    }
44
+
45
+    public String getId(){
46
+        return id;
47
+    }
48
+
49
+    public void setId(String id) {
50
+        this.id = id;
51
+    }
52
+
53
+    public String getLink() {
54
+        return link;
55
+    }
56
+
57
+    public void setLink(String link) {
58
+        this.link = link;
59
+    }
60
+
61
+    public String getSource() {
62
+        return source;
63
+    }
64
+
65
+    public void setSource(String source) {
66
+        this.source = source;
67
+    }
68
+
69
+    public String getText() {
70
+        return text;
71
+    }
72
+
73
+    public void setText(String text) {
74
+        this.text = text;
75
+    }
76
+
77
+    public Date getDate() {
78
+        return date;
79
+    }
80
+
81
+    public void setDate(Date date) {
82
+        this.date = date;
83
+    }
84
+
85
+    public String getHumanReadableRelativeTime() {
86
+        PrettyTime p = new PrettyTime();
87
+        return p.format(getDate());
88
+    }
89
+
90
+    public JSONObject serialize() throws JSONException {
91
+        JSONObject object = new JSONObject();
92
+        object.put("channel", channel);
93
+        object.put("id", id);
94
+        object.put("link", link);
95
+        object.put("text", text);
96
+        object.put("time", date);
97
+        object.put("source", source);
98
+        return object;
99
+    }
100
+
101
+    public static Date getTwitterDate(String date) throws ParseException {
102
+        final String twitter_format = "EEE MMM dd HH:mm:ss Z yyyy";
103
+        SimpleDateFormat sf = new SimpleDateFormat(twitter_format, Locale.ENGLISH);
104
+        sf.setLenient(true);
105
+        return sf.parse(date);
106
+    }
107
+
108
+    public static Flash deserialize(JSONObject jsonFlash) throws JSONException, ParseException {
109
+        Flash flash = new Flash(
110
+                (String) jsonFlash.get("channel"),
111
+                (String) jsonFlash.get("id"),
112
+                (String) jsonFlash.get("link"),
113
+                (String) jsonFlash.get("source"),
114
+                (String) jsonFlash.get("text"),
115
+                getTwitterDate((String) jsonFlash.get("time"))
116
+        );
117
+        return flash;
118
+    }
119
+}
+41
LibreNews-App/src/main/java/app/librenews/io/librenews/views/FlashView.java
... ...
@@ -0,0 +1,41 @@
1
+package app.librenews.io.librenews.views;
2
+
3
+import android.content.Intent;
4
+import android.preference.PreferenceActivity;
5
+import android.preference.PreferenceManager;
6
+import android.support.v4.widget.SwipeRefreshLayout;
7
+import android.support.v7.app.AppCompatActivity;
8
+import android.os.Bundle;
9
+import android.view.View;
10
+
11
+import app.librenews.io.librenews.R;
12
+import app.librenews.io.librenews.controllers.FlashManager;
13
+import app.librenews.io.librenews.controllers.SyncManager;
14
+
15
+public class FlashView extends AppCompatActivity {
16
+    FlashManager manager;
17
+
18
+    @Override
19
+    protected void onCreate(Bundle savedInstanceState) {
20
+        super.onCreate(savedInstanceState);
21
+        setContentView(R.layout.activity_flash_view);
22
+        manager = new FlashManager(this);
23
+        findViewById(R.id.settings_button).setOnClickListener(
24
+                new View.OnClickListener() {
25
+                    @Override
26
+                    public void onClick(View view) {
27
+                        Intent intent = new Intent(FlashView.this, SettingsActivity.class);
28
+                        startActivity(intent);
29
+                    }
30
+                }
31
+        );
32
+        final SwipeRefreshLayout srl = (SwipeRefreshLayout) findViewById(R.id.swiperefresh);
33
+        srl.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
34
+            @Override
35
+            public void onRefresh() {
36
+                new FlashManager(getApplicationContext()).refresh();
37
+                srl.setRefreshing(false);
38
+            }
39
+        });
40
+    }
41
+}
+37
LibreNews-App/src/main/java/app/librenews/io/librenews/views/SettingsActivity.java
... ...
@@ -0,0 +1,37 @@
1
+package app.librenews.io.librenews.views;
2
+
3
+import android.content.SharedPreferences;
4
+import android.os.Bundle;
5
+import android.preference.PreferenceActivity;
6
+import android.preference.PreferenceManager;
7
+import android.support.design.widget.FloatingActionButton;
8
+import android.support.design.widget.Snackbar;
9
+import android.support.v7.app.AppCompatActivity;
10
+import android.support.v7.widget.Toolbar;
11
+import android.view.View;
12
+
13
+import app.librenews.io.librenews.R;
14
+import app.librenews.io.librenews.controllers.FlashManager;
15
+import app.librenews.io.librenews.controllers.SyncManager;
16
+
17
+public class SettingsActivity extends PreferenceActivity {
18
+
19
+    @Override
20
+    protected void onCreate(Bundle savedInstanceState) {
21
+        super.onCreate(savedInstanceState);
22
+        addPreferencesFromResource(R.xml.preferences);
23
+        SharedPreferences.OnSharedPreferenceChangeListener spChanged = new
24
+                SharedPreferences.OnSharedPreferenceChangeListener() {
25
+                    @Override
26
+                    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
27
+                                                          String key) {
28
+                        if(key.equals("refresh_preference") || key.equals("automatically_refresh")){
29
+                            new SyncManager(getApplicationContext(), new FlashManager(getApplicationContext())).startSyncService();
30
+                        }
31
+                    }
32
+                };
33
+
34
+        PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).registerOnSharedPreferenceChangeListener(spChanged);
35
+    }
36
+
37
+}
+16
LibreNews-App/src/main/java/app/librenews/io/librenews/views/SettingsActivityFragment.java
... ...
@@ -0,0 +1,16 @@
1
+package app.librenews.io.librenews.views;
2
+
3
+import android.preference.PreferenceFragment;
4
+import android.os.Bundle;
5
+
6
+import app.librenews.io.librenews.R;
7
+
8
+public class SettingsActivityFragment extends PreferenceFragment {
9
+    @Override
10
+    public void onCreate(final Bundle savedInstanceState) {
11
+        super.onCreate(savedInstanceState);
12
+
13
+        // Load the preferences from an XML resource
14
+        addPreferencesFromResource(R.xml.preferences);
15
+    }
16
+}
+9
LibreNews-App/src/main/res/drawable/ic_alert.xml
... ...
@@ -0,0 +1,9 @@
1
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+        android:width="24dp"
3
+        android:height="24dp"
4
+        android:viewportWidth="24.0"
5
+        android:viewportHeight="24.0">
6
+    <path
7
+        android:fillColor="#FF000000"
8
+        android:pathData="M23,12l-2.44,-2.78 0.34,-3.68 -3.61,-0.82 -1.89,-3.18L12,3 8.6,1.54 6.71,4.72l-3.61,0.81 0.34,3.68L1,12l2.44,2.78 -0.34,3.69 3.61,0.82 1.89,3.18L12,21l3.4,1.46 1.89,-3.18 3.61,-0.82 -0.34,-3.68L23,12zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
9
+</vector>
+9
LibreNews-App/src/main/res/drawable/ic_debug.xml
... ...
@@ -0,0 +1,9 @@
1
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+        android:width="24dp"
3
+        android:height="24dp"
4
+        android:viewportWidth="24.0"
5
+        android:viewportHeight="24.0">
6
+    <path
7
+        android:fillColor="#FF000000"
8
+        android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/>
9
+</vector>
+9
LibreNews-App/src/main/res/drawable/ic_info_black_24dp.xml
... ...
@@ -0,0 +1,9 @@
1
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+    android:width="24dp"
3
+    android:height="24dp"
4
+    android:viewportHeight="24.0"
5
+    android:viewportWidth="24.0">
6
+    <path
7
+        android:fillColor="#FF000000"
8
+        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm1,15h-2v-6h2v6zm0,-8h-2V7h2v2z" />
9
+</vector>
+9
LibreNews-App/src/main/res/drawable/ic_notifications_black_24dp.xml
... ...
@@ -0,0 +1,9 @@
1
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+    android:width="24dp"
3
+    android:height="24dp"
4
+    android:viewportHeight="24.0"
5
+    android:viewportWidth="24.0">
6
+    <path
7
+        android:fillColor="#FF000000"
8
+        android:pathData="M11.5,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zm6.5,-6v-5.5c0,-3.07 -2.13,-5.64 -5,-6.32V3.5c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,2.67 10,3.5v0.68c-2.87,0.68 -5,3.25 -5,6.32V16l-2,2v1h17v-1l-2,-2z" />
9
+</vector>
+9
LibreNews-App/src/main/res/drawable/ic_sync_black_24dp.xml
... ...
@@ -0,0 +1,9 @@
1
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+    android:width="24dp"
3
+    android:height="24dp"
4
+    android:viewportHeight="24.0"
5
+    android:viewportWidth="24.0">
6
+    <path
7
+        android:fillColor="#FF000000"
8
+        android:pathData="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01,-.25 1.97,-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0,-4.42,-3.58,-8,-8,-8zm0 14c-3.31 0,-6,-2.69,-6,-6 0,-1.01.25,-1.97.7,-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4,-4,-4,-4v3z" />
9
+</vector>
+88
LibreNews-App/src/main/res/layout/activity_flash_view.xml
... ...
@@ -0,0 +1,88 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
3
+    xmlns:tools="http://schemas.android.com/tools"
4
+    xmlns:app="http://schemas.android.com/apk/res-auto"
5
+    android:layout_width="match_parent"
6
+    android:layout_height="match_parent">
7
+
8
+    <android.support.constraint.ConstraintLayout
9
+        android:layout_width="match_parent"
10
+        android:layout_height="match_parent">
11
+
12
+        <android.support.v7.widget.Toolbar
13
+            android:id="@+id/toolbar"
14
+            android:layout_width="0dp"
15
+            android:layout_height="wrap_content"
16
+            android:background="@color/colorPrimarySub"
17
+            android:minHeight="?attr/actionBarSize"
18
+            android:theme="?attr/actionBarTheme"
19
+            app:layout_constraintLeft_toLeftOf="parent"
20
+            app:layout_constraintRight_toRightOf="parent"
21
+            app:layout_constraintTop_toTopOf="parent" />
22
+
23
+        <Button
24
+            android:id="@+id/settings_button"
25
+            style="@style/Widget.AppCompat.Button.Borderless.Colored"
26
+            android:layout_width="wrap_content"
27
+            android:layout_height="wrap_content"
28
+            android:layout_marginBottom="8dp"
29
+            android:layout_marginEnd="8dp"
30
+            android:layout_marginRight="8dp"
31
+            android:layout_marginTop="8dp"
32
+            android:text="@string/action_settings"
33
+            android:textColor="@android:color/background_light"
34
+            app:layout_constraintBottom_toBottomOf="@+id/toolbar"
35
+            app:layout_constraintRight_toRightOf="@+id/toolbar"
36
+            app:layout_constraintTop_toTopOf="@+id/toolbar" />
37
+
38
+        <TextView
39
+            android:id="@+id/server_name"
40
+            android:layout_width="0dp"
41
+            android:layout_height="wrap_content"
42
+            android:layout_marginBottom="8dp"
43
+            android:layout_marginLeft="16dp"
44
+            android:layout_marginStart="16dp"
45
+            android:layout_marginTop="8dp"
46
+            android:text="[server name]"
47
+            android:textAppearance="@style/TextAppearance.AppCompat.Medium.Inverse"
48
+            app:layout_constraintBottom_toBottomOf="@+id/toolbar"
49
+            app:layout_constraintLeft_toLeftOf="@+id/toolbar"
50
+            app:layout_constraintTop_toTopOf="@+id/toolbar"
51
+            app:layout_constraintVertical_bias="0.444" />
52
+
53
+        <android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
54
+            android:id="@+id/swiperefresh"
55
+            android:layout_width="360dp"
56
+            android:layout_height="0dp"
57
+            app:layout_constraintTop_toBottomOf="@+id/toolbar"
58
+            android:layout_marginLeft="8dp"
59
+            app:layout_constraintLeft_toLeftOf="parent"
60
+            app:layout_constraintBottom_toBottomOf="parent"
61
+            android:layout_marginStart="8dp"
62
+            app:layout_constraintVertical_bias="0.5">
63
+
64
+            <ScrollView
65
+                android:layout_width="match_parent"
66
+                android:layout_height="8dp"
67
+                tools:layout_editor_absoluteX="8dp"
68
+                tools:layout_editor_absoluteY="137dp">
69
+
70
+                <LinearLayout
71
+                    android:layout_width="match_parent"
72
+                    android:layout_height="wrap_content"
73
+                    android:orientation="vertical">
74
+
75
+                    <android.support.constraint.ConstraintLayout
76
+                        android:layout_width="match_parent"
77
+                        android:layout_height="match_parent">
78
+
79
+                    </android.support.constraint.ConstraintLayout>
80
+                </LinearLayout>
81
+            </ScrollView>
82
+
83
+        </android.support.v4.widget.SwipeRefreshLayout>
84
+
85
+
86
+    </android.support.constraint.ConstraintLayout>
87
+
88
+</android.support.design.widget.CoordinatorLayout>
+11
LibreNews-App/src/main/res/layout/fragment_settings.xml
... ...
@@ -0,0 +1,11 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3
+    xmlns:app="http://schemas.android.com/apk/res-auto"
4
+    xmlns:tools="http://schemas.android.com/tools"
5
+    android:layout_width="match_parent"
6
+    android:layout_height="match_parent"
7
+    tools:context="app.librenews.io.librenews.views.SettingsActivityFragment"
8
+    tools:showIn="@layout/activity_settings">
9
+
10
+
11
+</android.support.constraint.ConstraintLayout>
+10
LibreNews-App/src/main/res/menu/menu_settings.xml
... ...
@@ -0,0 +1,10 @@
1
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
2
+    xmlns:app="http://schemas.android.com/apk/res-auto"
3
+    xmlns:tools="http://schemas.android.com/tools"
4
+    tools:context="app.librenews.io.librenews.views.SettingsActivity">
5
+    <item
6
+        android:id="@+id/action_settings"
7
+        android:orderInCategory="100"
8
+        android:title="@string/action_settings"
9
+        app:showAsAction="never" />
10
+</menu>
BIN
LibreNews-App/src/main/res/mipmap-hdpi/ic_launcher.png
BIN
LibreNews-App/src/main/res/mipmap-hdpi/ic_launcher_round.png
BIN
LibreNews-App/src/main/res/mipmap-mdpi/ic_launcher.png
BIN
LibreNews-App/src/main/res/mipmap-mdpi/ic_launcher_round.png
BIN
LibreNews-App/src/main/res/mipmap-xhdpi/ic_launcher.png
BIN
LibreNews-App/src/main/res/mipmap-xhdpi/ic_launcher_round.png
BIN
LibreNews-App/src/main/res/mipmap-xxhdpi/ic_launcher.png
BIN
LibreNews-App/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
BIN
LibreNews-App/src/main/res/mipmap-xxxhdpi/ic_launcher.png
BIN
LibreNews-App/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
+31
LibreNews-App/src/main/res/values/arrays.xml
... ...
@@ -0,0 +1,31 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<resources>
3
+    <string-array name="refresh_rates">
4
+        <item>1 minute</item>
5
+        <item>2 minutes</item>
6
+        <item>5 minutes</item>
7
+        <item>10 minutes</item>
8
+        <item>15 minutes</item>
9
+        <item>30 minutes</item>
10
+        <item>1 hour</item>
11
+        <item>2 hours</item>
12
+        <item>3 hours</item>
13
+        <item>6 hours</item>
14
+        <item>12 hours</item>
15
+        <item>24 hours</item>
16
+    </string-array>
17
+    <string-array name="refresh_values">
18
+        <item>1</item>
19
+        <item>2</item>
20
+        <item>5</item>
21
+        <item>10</item>
22
+        <item>15</item>
23
+        <item>30</item>
24
+        <item>60</item>
25
+        <item>120</item>
26
+        <item>180</item>
27
+        <item>360</item>
28
+        <item>720</item>
29
+        <item>1440</item>
30
+    </string-array>
31
+</resources>
+8
LibreNews-App/src/main/res/values/attrs_flash_embed_view.xml
... ...
@@ -0,0 +1,8 @@
1
+<resources>
2
+    <declare-styleable name="FlashEmbedView">
3
+        <attr name="exampleString" format="string" />
4
+        <attr name="exampleDimension" format="dimension" />
5
+        <attr name="exampleColor" format="color" />
6
+        <attr name="exampleDrawable" format="color|reference" />
7
+    </declare-styleable>
8
+</resources>
+7
LibreNews-App/src/main/res/values/colors.xml
... ...
@@ -0,0 +1,7 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<resources>
3
+    <color name="colorPrimary">#3F51B5</color>
4
+    <color name="colorPrimaryDark">#303F9F</color>
5
+    <color name="colorAccent">#FF4081</color>
6
+    <color name="colorPrimarySub">#5b64c0</color>
7
+</resources>
+3
LibreNews-App/src/main/res/values/dimens.xml
... ...
@@ -0,0 +1,3 @@
1
+<resources>
2
+    <dimen name="fab_margin">16dp</dimen>
3
+</resources>
+96
LibreNews-App/src/main/res/values/strings.xml
... ...
@@ -0,0 +1,96 @@
1
+<resources>
2
+    <string name="app_name">LibreNews</string>
3
+    <string name="server_settings_label">Server Settings</string>
4
+    <string name="default_server_url">https://librenews.io/api</string>
5
+    <string name="server_url_label">Server</string>
6
+    <string name="refresh_rate_label">Refresh rate</string>
7
+    <string name="notification_preferences_label">Notification Settings</string>
8
+    <string name="notifications_enabled_label">Notifications enabled</string>
9
+    <string name="auto_refresh_label">Automatically refresh</string>
10
+    <string name="server_url_description">The URL of the server to get newsflashes from. Must be HTTPS.</string>
11
+    <string name="auto_refresh_description">Whether LibreNews should automatically refresh in the background.</string>
12
+    <string name="refresh_rate_description">How frequently LibreNews should contact the newsflash server in the background to check for new notifications.</string>
13
+    <string name="notification_sound_label">Notification sound</string>
14
+    <string name="notification_sound_description">The sound that LibreNews will make when a notification is received.</string>
15
+    <string name="notifications_enabled_description">Whether LibreNews shoud send you notifications.</string>
16
+    <string name="internal_storage_setup_fail">Unable to set up internal storage!</string>
17
+    <string name="internal_storage_read_fail">Unable to read from internal storage!</string>
18
+    <string name="invalid_server_url">Invalid server URL!</string>
19
+    <string name="debug_label">Debug</string>
20
+    <string name="debug_mode_label">Debug mode</string>
21
+    <string name="debug_mode_description">Whether LibreNews should send you lots of debug messages (via notifications)</string>
22
+    <string name="title_activity_settings">Settings</string>
23
+
24
+    <!-- Strings related to Settings -->
25
+
26
+    <!-- Example General settings -->
27
+    <string name="pref_header_general">General</string>
28
+
29
+    <string name="pref_title_social_recommendations">Enable social recommendations</string>
30
+    <string name="pref_description_social_recommendations">Recommendations for people to contact
31
+        based on your message history
32
+    </string>
33
+
34
+    <string name="pref_title_display_name">Display name</string>
35
+    <string name="pref_default_display_name">John Smith</string>
36
+
37
+    <string name="pref_title_add_friends_to_messages">Add friends to messages</string>
38
+    <string-array name="pref_example_list_titles">
39
+        <item>Always</item>
40
+        <item>When possible</item>
41
+        <item>Never</item>
42
+    </string-array>
43
+    <string-array name="pref_example_list_values">
44
+        <item>1</item>
45
+        <item>0</item>
46
+        <item>-1</item>
47
+    </string-array>
48
+
49
+    <!-- Example settings for Data & Sync -->
50
+    <string name="pref_header_data_sync">Data &amp; sync</string>
51
+
52
+    <string name="pref_title_sync_frequency">Sync frequency</string>
53
+    <string-array name="pref_sync_frequency_titles">
54
+        <item>15 minutes</item>
55
+        <item>30 minutes</item>
56
+        <item>1 hour</item>
57
+        <item>3 hours</item>
58
+        <item>6 hours</item>
59
+        <item>Never</item>
60
+    </string-array>
61
+    <string-array name="pref_sync_frequency_values">
62
+        <item>15</item>
63
+        <item>30</item>
64
+        <item>60</item>
65
+        <item>180</item>
66
+        <item>360</item>
67
+        <item>-1</item>
68
+    </string-array>
69
+
70
+    <string-array name="list_preference_entries">
71
+        <item>Entry 1</item>
72
+        <item>Entry 2</item>
73
+        <item>Entry 3</item>
74
+    </string-array>
75
+
76
+    <string-array name="list_preference_entry_values">
77
+        <item>1</item>
78
+        <item>2</item>
79
+        <item>3</item>
80
+    </string-array>
81
+
82
+    <string-array name="multi_select_list_preference_default_value" />
83
+
84
+    <string name="pref_title_system_sync_settings">System sync settings</string>
85
+
86
+    <!-- Example settings for Notifications -->
87
+    <string name="pref_header_notifications">Notifications</string>
88
+
89
+    <string name="pref_title_new_message_notifications">New message notifications</string>
90
+
91
+    <string name="pref_title_ringtone">Ringtone</string>
92
+    <string name="pref_ringtone_silent">Silent</string>
93
+
94
+    <string name="pref_title_vibrate">Vibrate</string>
95
+    <string name="action_settings">Settings</string>
96
+</resources>
+20
LibreNews-App/src/main/res/values/styles.xml
... ...
@@ -0,0 +1,20 @@
1
+<resources>
2
+
3
+    <!-- Base application theme. -->
4
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
5
+        <!-- Customize your theme here. -->
6
+        <item name="colorPrimary">@color/colorPrimary</item>
7
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
8
+        <item name="colorAccent">@color/colorAccent</item>
9
+    </style>
10
+
11
+    <style name="AppTheme.NoActionBar">
12
+        <item name="windowActionBar">false</item>
13
+        <item name="windowNoTitle">true</item>
14
+    </style>
15
+
16
+    <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
17
+
18
+    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
19
+
20
+</resources>
+52
LibreNews-App/src/main/res/xml/preferences.xml
... ...
@@ -0,0 +1,52 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
3
+
4
+    <PreferenceCategory android:title="@string/server_settings_label">
5
+
6
+        <EditTextPreference
7
+            android:id="@+id/server_url"
8
+            android:defaultValue="@string/default_server_url"
9
+            android:key="server_url"
10
+            android:selectAllOnFocus="true"
11
+            android:singleLine="true"
12
+            android:summary="@string/server_url_description"
13
+            android:title="@string/server_url_label" />
14
+        <SwitchPreference
15
+            android:defaultValue="true"
16
+            android:key="automatically_refresh"
17
+            android:summary="@string/auto_refresh_description"
18
+            android:title="@string/auto_refresh_label" />
19
+        <ListPreference
20
+            android:defaultValue="5"
21
+            android:dependency="automatically_refresh"
22
+            android:entries="@array/refresh_rates"
23
+            android:entryValues="@array/refresh_values"
24
+            android:key="refresh_preference"
25
+            android:summary="@string/refresh_rate_description"
26
+            android:title="@string/refresh_rate_label" />
27
+    </PreferenceCategory>
28
+    <PreferenceCategory android:title="@string/notification_preferences_label">
29
+
30
+        <SwitchPreference
31
+            android:id="@+id/notifications_enabled"
32
+            android:defaultValue="true"
33
+            android:key="notifications_enabled"
34
+            android:summary="@string/notifications_enabled_description"
35
+            android:title="@string/notifications_enabled_label" />
36
+        <RingtonePreference
37
+            android:defaultValue=""
38
+            android:dependency="notifications_enabled"
39
+            android:key="notification_sound"
40
+            android:ringtoneType="notification|alarm"
41
+            android:title="@string/notification_sound_label" />
42
+    </PreferenceCategory>
43
+    <PreferenceCategory android:title="@string/debug_label">
44
+
45
+        <SwitchPreference
46
+            android:id="@+id/debug"
47
+            android:defaultValue="false"
48
+            android:key="debug"
49
+            android:summary="@string/debug_mode_description"
50
+            android:title="@string/debug_mode_label" />
51
+    </PreferenceCategory>
52
+</PreferenceScreen>
+17
LibreNews-App/src/test/java/app/librenews/io/librenews/ExampleUnitTest.java
... ...
@@ -0,0 +1,17 @@
1
+package app.librenews.io.librenews;
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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
11
+ */
12
+public class ExampleUnitTest {
13
+    @Test
14
+    public void addition_isCorrect() throws Exception {
15
+        assertEquals(4, 2 + 2);
16
+    }
17
+}
+23
build.gradle
... ...
@@ -0,0 +1,23 @@
1
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
2
+
3
+buildscript {
4
+    repositories {
5
+        jcenter()
6
+    }
7
+    dependencies {
8
+        classpath 'com.android.tools.build:gradle:2.3.3'
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
+        jcenter()
18
+    }
19
+}
20
+
21
+task clean(type: Delete) {
22
+    delete rootProject.buildDir
23
+}
+17
gradle.properties
... ...
@@ -0,0 +1,17 @@
1
+# Project-wide Gradle settings.
2
+
3
+# IDE (e.g. Android Studio) users:
4
+# Gradle settings configured through the IDE *will override*
5
+# any settings specified in this file.
6
+
7
+# For more details on how to configure your build environment visit
8
+# http://www.gradle.org/docs/current/userguide/build_environment.html
9
+
10
+# Specifies the JVM arguments used for the daemon process.
11
+# The setting is particularly useful for tweaking memory settings.
12
+org.gradle.jvmargs=-Xmx1536m
13
+
14
+# When configured, Gradle will run in incubating parallel mode.
15
+# This option should only be used with decoupled projects. More details, visit
16
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17
+# org.gradle.parallel=true
BIN
gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
+6
gradle/wrapper/gradle-wrapper.properties
... ...
@@ -0,0 +1,6 @@
1
+#Fri Jul 14 22:41:16 EDT 2017
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-3.3-all.zip
+160
gradlew
... ...
@@ -0,0 +1,160 @@
1
+#!/usr/bin/env bash
2
+
3
+##############################################################################
4
+##
5
+##  Gradle start up script for UN*X
6
+##
7
+##############################################################################
8
+
9
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10
+DEFAULT_JVM_OPTS=""
11
+
12
+APP_NAME="Gradle"
13
+APP_BASE_NAME=`basename "$0"`
14
+
15
+# Use the maximum available, or set MAX_FD != -1 to use that value.
16
+MAX_FD="maximum"
17
+
18
+warn ( ) {
19
+    echo "$*"
20
+}
21
+
22
+die ( ) {
23
+    echo
24
+    echo "$*"
25
+    echo
26
+    exit 1
27
+}
28
+
29
+# OS specific support (must be 'true' or 'false').
30
+cygwin=false
31
+msys=false
32
+darwin=false
33
+case "`uname`" in
34
+  CYGWIN* )
35
+    cygwin=true
36
+    ;;
37
+  Darwin* )
38
+    darwin=true
39
+    ;;
40
+  MINGW* )
41
+    msys=true
42
+    ;;
43
+esac
44
+
45
+# Attempt to set APP_HOME
46
+# Resolve links: $0 may be a link
47
+PRG="$0"
48
+# Need this for relative symlinks.
49
+while [ -h "$PRG" ] ; do
50
+    ls=`ls -ld "$PRG"`
51
+    link=`expr "$ls" : '.*-> \(.*\)$'`
52
+    if expr "$link" : '/.*' > /dev/null; then
53
+        PRG="$link"
54
+    else
55
+        PRG=`dirname "$PRG"`"/$link"
56
+    fi
57
+done
58
+SAVED="`pwd`"
59
+cd "`dirname \"$PRG\"`/" >/dev/null
60
+APP_HOME="`pwd -P`"
61
+cd "$SAVED" >/dev/null
62
+
63
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64
+
65
+# Determine the Java command to use to start the JVM.
66
+if [ -n "$JAVA_HOME" ] ; then
67
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68
+        # IBM's JDK on AIX uses strange locations for the executables
69
+        JAVACMD="$JAVA_HOME/jre/sh/java"
70
+    else
71
+        JAVACMD="$JAVA_HOME/bin/java"
72
+    fi
73
+    if [ ! -x "$JAVACMD" ] ; then
74
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75
+
76
+Please set the JAVA_HOME variable in your environment to match the
77
+location of your Java installation."
78
+    fi
79
+else
80
+    JAVACMD="java"
81
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82
+
83
+Please set the JAVA_HOME variable in your environment to match the
84
+location of your Java installation."
85
+fi
86
+
87
+# Increase the maximum file descriptors if we can.
88
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89
+    MAX_FD_LIMIT=`ulimit -H -n`
90
+    if [ $? -eq 0 ] ; then
91
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92
+            MAX_FD="$MAX_FD_LIMIT"
93
+        fi
94
+        ulimit -n $MAX_FD
95
+        if [ $? -ne 0 ] ; then
96
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
97
+        fi
98
+    else
99
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100
+    fi
101
+fi
102
+
103
+# For Darwin, add options to specify how the application appears in the dock
104
+if $darwin; then
105
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106
+fi
107
+
108
+# For Cygwin, switch paths to Windows format before running java
109
+if $cygwin ; then
110
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112
+    JAVACMD=`cygpath --unix "$JAVACMD"`
113
+
114
+    # We build the pattern for arguments to be converted via cygpath
115
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116
+    SEP=""
117
+    for dir in $ROOTDIRSRAW ; do
118
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
119
+        SEP="|"
120
+    done
121
+    OURCYGPATTERN="(^($ROOTDIRS))"
122
+    # Add a user-defined pattern to the cygpath arguments
123
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125
+    fi
126
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
127
+    i=0
128
+    for arg in "$@" ; do
129
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
131
+
132
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
133
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134
+        else
135
+            eval `echo args$i`="\"$arg\""
136
+        fi
137
+        i=$((i+1))
138
+    done
139
+    case $i in
140
+        (0) set -- ;;
141
+        (1) set -- "$args0" ;;
142
+        (2) set -- "$args0" "$args1" ;;
143
+        (3) set -- "$args0" "$args1" "$args2" ;;
144
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150
+    esac
151
+fi
152
+
153
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154
+function splitJvmOpts() {
155
+    JVM_OPTS=("$@")
156
+}
157
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159
+
160
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
+90
gradlew.bat
... ...
@@ -0,0 +1,90 @@
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
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12
+set DEFAULT_JVM_OPTS=
13
+
14
+set DIRNAME=%~dp0
15
+if "%DIRNAME%" == "" set DIRNAME=.
16
+set APP_BASE_NAME=%~n0
17
+set APP_HOME=%DIRNAME%
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 Windowz variants
50
+
51
+if not "%OS%" == "Windows_NT" goto win9xME_args
52
+if "%@eval[2+2]" == "4" goto 4NT_args
53
+
54
+:win9xME_args
55
+@rem Slurp the command line arguments.
56
+set CMD_LINE_ARGS=
57
+set _SKIP=2
58
+
59
+:win9xME_args_slurp
60
+if "x%~1" == "x" goto execute
61
+
62
+set CMD_LINE_ARGS=%*
63
+goto execute
64
+
65
+:4NT_args
66
+@rem Get arguments from the 4NT Shell from JP Software
67
+set CMD_LINE_ARGS=%$
68
+
69
+:execute
70
+@rem Setup the command line
71
+
72
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73
+
74
+@rem Execute Gradle
75
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76
+
77
+:end
78
+@rem End local scope for the variables with windows NT shell
79
+if "%ERRORLEVEL%"=="0" goto mainEnd
80
+
81
+:fail
82
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83
+rem the _cmd.exe /c_ return code!
84
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85
+exit /b 1
86
+
87
+:mainEnd
88
+if "%OS%"=="Windows_NT" endlocal
89
+
90
+:omega
+1
settings.gradle
... ...
@@ -0,0 +1 @@
1
+include ':LibreNews-App'