*
Calling TopicTask's {@link ParseTask#execute execute} method needs to have profile's url * as String parameter!
*/ private class ParseDownloadPageTask extends ParseTask { + private Download.DownloadItemType type; + private Download download; + @Override protected void onPreExecute() { if (!isLoadingMore) progressBar.setVisibility(ProgressBar.VISIBLE); @@ -172,71 +180,92 @@ public class DownloadsActivity extends BaseActivity implements DownloadsAdapter. @Override protected void parse(Document downloadPage) throws ParseException { - if (downloadsTitle == null || Objects.equals(downloadsTitle, "")) - downloadsTitle = downloadPage.select("div.nav>b>a.nav").last().text(); - - //Removes loading item - if (isLoadingMore) { - if (parsedDownloads.size() > 0) parsedDownloads.remove(parsedDownloads.size() - 1); - } + try{ + if (downloadsTitle == null || Objects.equals(downloadsTitle, "")) + downloadsTitle = downloadPage.select("div.nav>b>a.nav").last().text(); - Download.DownloadItemType type; - if (ThmmyPage.resolvePageCategory(Uri.parse(url)).is(ThmmyPage. - PageCategory.DOWNLOADS_CATEGORY)) - type = Download.DownloadItemType.DOWNLOADS_CATEGORY; - else type = Download.DownloadItemType.DOWNLOADS_FILE; - - Elements pages = downloadPage.select("a.navPages"); - if (pages != null) { - for (Element page : pages) { - int pageNumber = Integer.parseInt(page.text()); - if (pageNumber > numberOfPages) numberOfPages = pageNumber; + //Removes loading item + if (isLoadingMore) { + if (parsedDownloads.size() > 0) parsedDownloads.remove(parsedDownloads.size() - 1); } - } else numberOfPages = 1; - - Elements rows = downloadPage.select("table.tborder>tbody>tr"); - if (type == Download.DownloadItemType.DOWNLOADS_CATEGORY) { - Elements navigationLinks = downloadPage.select("div.nav>b"); - for (Element row : rows) { - if (row.select("td").size() == 1) continue; - - String url = row.select("b>a").first().attr("href"), - title = row.select("b>a").first().text(), - subtitle = row.select("div.smalltext:not(:has(a))").text(); - if (!row.select("td").last().hasClass("windowbg2")) { - if (navigationLinks.size() < 4) { - - parsedDownloads.add(new Download(type, url, title, subtitle, null, - true, null)); + + if (ThmmyPage.resolvePageCategory(Uri.parse(url)).is(ThmmyPage.PageCategory.DOWNLOADS_CATEGORY)) + type = Download.DownloadItemType.DOWNLOADS_CATEGORY; + else + type = Download.DownloadItemType.DOWNLOADS_FILE; + + Elements pages = downloadPage.select("a.navPages"); + if (pages != null) { + for (Element page : pages) { + int pageNumber = Integer.parseInt(page.text()); + if (pageNumber > numberOfPages) numberOfPages = pageNumber; + } + } else numberOfPages = 1; + + Elements rows = downloadPage.select("table.tborder>tbody>tr"); + if (type == Download.DownloadItemType.DOWNLOADS_CATEGORY) { + Elements navigationLinks = downloadPage.select("div.nav>b"); + for (Element row : rows) { + if (row.select("td").size() == 1) continue; + + String url = row.select("b>a").first().attr("href"), + title = row.select("b>a").first().text(), + subtitle = row.select("div.smalltext:not(:has(a))").text(); + if (!row.select("td").last().hasClass("windowbg2")) { + if (navigationLinks.size() < 4) { + + parsedDownloads.add(new Download(type, url, title, subtitle, null, + true, null)); + } else { + String stats = row.text(); + stats = stats.replace(title, "").replace(subtitle, "").trim(); + parsedDownloads.add(new Download(type, url, title, subtitle, stats, + false, null)); + } } else { String stats = row.text(); stats = stats.replace(title, "").replace(subtitle, "").trim(); parsedDownloads.add(new Download(type, url, title, subtitle, stats, false, null)); } - } else { - String stats = row.text(); - stats = stats.replace(title, "").replace(subtitle, "").trim(); - parsedDownloads.add(new Download(type, url, title, subtitle, stats, - false, null)); } + } else { + download = new Download(type, + rows.select("b>a").first().attr("href"), + rows.select("b>a").first().text(), + rows.select("div.smalltext:not(:has(a))").text(), + rows.select("span:not(:has(a))").first().text(), + false, + rows.select("span:has(a)").first().text()); + parsedDownloads.add(download); } - } else { - parsedDownloads.add(new Download(type, - rows.select("b>a").first().attr("href"), - rows.select("b>a").first().text(), - rows.select("div.smalltext:not(:has(a))").text(), - rows.select("span:not(:has(a))").first().text(), - false, - rows.select("span:has(a)").first().text())); + }catch(Exception e){ + throw new ParseException("Parsing failed (DownloadsActivity)"); } } + @Override + protected void postParsing() { + if (type == Download.DownloadItemType.DOWNLOADS_FILE) { + OkHttpClient client = BaseApplication.getInstance().getClient(); + String fileName = null; + try { + Response response = client.newCall(new Request.Builder().url(download.getUrl()).build()).execute(); + String contentDisposition = response.headers("Content-Disposition").toString(); //check if link provides an attachment + if (contentDisposition.contains("attachment")) + fileName = contentDisposition.split("\"")[1]; + download.setFileName(fileName); + } catch (Exception e) { + Timber.e(e, "Couldn't extract fileName."); + } + } + } @Override - protected void postParsing(ResultCode result) { - if (downloadsTitle != null && !Objects.equals(downloadsTitle, "") && - toolbar.getTitle() != downloadsTitle) + protected void postExecution(ResultCode result) { + if (downloadsTitle != null && !downloadsTitle.equals("") + && !downloadsTitle.equals("Αρχεία για λήψη") + && toolbar.getTitle() != downloadsTitle) toolbar.setTitle(downloadsTitle); ++pagesLoaded; diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsAdapter.java index 7a7bf17f..5395b447 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsAdapter.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsAdapter.java @@ -127,8 +127,8 @@ class DownloadsAdapter extends RecyclerView.AdapterTopicTask's {@link AsyncTask#execute execute} method needs a topic's url as String * parameter.
*/ - class TopicTask extends AsyncTaskClass contains the methods:
PostNotification model is described by its post's id, its topic's id & title and by its poster + *
. + */ +public class PostNotification { + final int postId; + final int topicId; + final String topicTitle; + final String poster; + + // Suppresses default constructor + @SuppressWarnings("unused") + PostNotification() { + this.postId = -1; + this.topicId = -1; + this.topicTitle = null; + this.poster = null; + } + + /** + * Constructor specifying all class variables necessary to summarize this post. All variables + * are declared final, once assigned they cannot change. + * + * @param postId this post's id + * @param topicId this post's topicId + * @param topicTitle this post's topicTitle + * @param poster username of this post's author + */ + public PostNotification(int postId, int topicId, String topicTitle, String poster) { + this.postId = postId; + this.topicId = topicId; + this.topicTitle = topicTitle; + this.poster = poster; + } + + /** + * Gets this post's Id. + * + * @return this post's Id + */ + public int getPostId() { + return postId; + } + + /** + * Gets this post's topicId. + * + * @return this post's topicId + */ + public int getTopicId() { + return topicId; + } + + /** + * Gets this post's topicTitle. + * + * @return this post's topicTitle + */ + public String getTopicTitle() { + return topicTitle; + } + + /** + * Gets username of this post's author. + * + * @return username of this post's author + */ + public String getPoster() { + return poster; + } +} + + + + + diff --git a/app/src/main/java/gr/thmmy/mthmmy/model/ThmmyPage.java b/app/src/main/java/gr/thmmy/mthmmy/model/ThmmyPage.java index 8962f2b9..bc451fd1 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/model/ThmmyPage.java +++ b/app/src/main/java/gr/thmmy/mthmmy/model/ThmmyPage.java @@ -187,4 +187,13 @@ public class ThmmyPage { } return null; } + + /** + * This method gets a VALID topic url and strips it off any unnecessary stuff (e.g. wap2). + * @param topicUrl a valid topic url + * @return sanitized topic url + */ + public static String sanitizeTopicUrl(String topicUrl) { + return topicUrl.replace("action=printpage;","").replace("wap2",""); + } } diff --git a/app/src/main/java/gr/thmmy/mthmmy/receiver/Receiver.java b/app/src/main/java/gr/thmmy/mthmmy/receiver/Receiver.java deleted file mode 100644 index 971475ff..00000000 --- a/app/src/main/java/gr/thmmy/mthmmy/receiver/Receiver.java +++ /dev/null @@ -1,80 +0,0 @@ -package gr.thmmy.mthmmy.receiver; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.support.v7.app.NotificationCompat; -import android.webkit.MimeTypeMap; - -import java.io.File; - -import timber.log.Timber; - -import static gr.thmmy.mthmmy.services.DownloadService.ACTION_DOWNLOAD; -import static gr.thmmy.mthmmy.services.DownloadService.COMPLETED; -import static gr.thmmy.mthmmy.services.DownloadService.EXTRA_DOWNLOAD_ID; -import static gr.thmmy.mthmmy.services.DownloadService.EXTRA_DOWNLOAD_STATE; -import static gr.thmmy.mthmmy.services.DownloadService.EXTRA_FILE_NAME; -import static gr.thmmy.mthmmy.services.DownloadService.EXTRA_NOTIFICATION_TEXT; -import static gr.thmmy.mthmmy.services.DownloadService.EXTRA_NOTIFICATION_TICKER; -import static gr.thmmy.mthmmy.services.DownloadService.EXTRA_NOTIFICATION_TITLE; -import static gr.thmmy.mthmmy.services.DownloadService.SAVE_DIR; -import static gr.thmmy.mthmmy.services.DownloadService.STARTED; - -public class Receiver extends BroadcastReceiver { - - public Receiver() {} - - @Override - public void onReceive(Context context, Intent intent) { - NotificationCompat.Builder builder = new NotificationCompat.Builder(context); - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - - if (intent.getAction().equals(ACTION_DOWNLOAD)) { - Bundle extras = intent.getExtras(); - int id = extras.getInt(EXTRA_DOWNLOAD_ID); - String state = extras.getString(EXTRA_DOWNLOAD_STATE, "NONE"); - String title = extras.getString(EXTRA_NOTIFICATION_TITLE); - String text = extras.getString(EXTRA_NOTIFICATION_TEXT); - String ticker = extras.getString(EXTRA_NOTIFICATION_TICKER); - - builder.setContentTitle(title) - .setContentText(text) - .setTicker(ticker) - .setAutoCancel(true); - - if (state.equals(STARTED)) - builder.setOngoing(true) - .setSmallIcon(android.R.drawable.stat_sys_download); - else if (state.equals(COMPLETED)) { - String fileName = extras.getString(EXTRA_FILE_NAME, "NONE"); - - File file = new File(SAVE_DIR, fileName); - if (file.exists()) { - String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension( - MimeTypeMap.getFileExtensionFromUrl(file.getAbsolutePath())); - - - Intent chooserIntent = new Intent(Intent.ACTION_VIEW); - chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - chooserIntent.setDataAndType(Uri.fromFile(file), type); - Intent chooser = Intent.createChooser(chooserIntent, "Open With..."); - - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, chooser, PendingIntent.FLAG_CANCEL_CURRENT); - builder.setContentIntent(pendingIntent) - .setSmallIcon(android.R.drawable.stat_sys_download_done); - - } else - Timber.w("File doesn't exist."); - } - Notification notification = builder.build(); - notificationManager.notify(id, notification); - } - } - -} diff --git a/app/src/main/java/gr/thmmy/mthmmy/services/DownloadHelper.java b/app/src/main/java/gr/thmmy/mthmmy/services/DownloadHelper.java new file mode 100644 index 00000000..1e1a5c31 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/services/DownloadHelper.java @@ -0,0 +1,73 @@ +package gr.thmmy.mthmmy.services; + +import android.app.DownloadManager; +import android.content.Context; +import android.net.Uri; +import android.os.Environment; +import android.widget.Toast; + +import java.io.File; + +import gr.thmmy.mthmmy.base.BaseApplication; +import gr.thmmy.mthmmy.model.ThmmyFile; +import okhttp3.Cookie; +import timber.log.Timber; + +import static gr.thmmy.mthmmy.utils.FileUtils.getMimeType; + +/** + * Not an actual service, but simply a helper class that adds a download to the queue of Android's + * DownloadManager system service. + */ +public class DownloadHelper { + public static final File SAVE_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + + public static void enqueueDownload(ThmmyFile thmmyFile){ + Context applicationContext = BaseApplication.getInstance().getApplicationContext(); + Toast.makeText(applicationContext, "Download started!", Toast.LENGTH_SHORT).show(); + + try { + String fileName = renameFileIfExists(thmmyFile.getFilename()); + Uri downloadURI = Uri.parse(thmmyFile.getFileUrl().toString()); + + DownloadManager downloadManager = (DownloadManager)applicationContext.getSystemService(Context.DOWNLOAD_SERVICE); + DownloadManager.Request request = new DownloadManager.Request(downloadURI); + + Cookie thmmyCookie = BaseApplication.getInstance().getSessionManager().getThmmyCookie(); + request.addRequestHeader("Cookie", thmmyCookie.name() + "=" + thmmyCookie.value()); + request.setTitle(fileName); + request.setMimeType(getMimeType(fileName)); + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + request.setDestinationInExternalPublicDir(SAVE_DIR.getName(), fileName); + request.allowScanningByMediaScanner(); + + downloadManager.enqueue(request); + } catch (Exception e) { + Toast.makeText(applicationContext, "Download failed...", Toast.LENGTH_SHORT).show(); + Timber.e(e, "Exception while enqueuing download."); + } + } + + private static String renameFileIfExists(String originalFileName) { + final String dirPath = SAVE_DIR.getAbsolutePath(); + File file = new File(dirPath, originalFileName); + + String nameFormat; + String[] tokens = originalFileName.split("\\.(?=[^.]+$)"); + + if (tokens.length != 2) { + Timber.w("Couldn't get file extension..."); + nameFormat = originalFileName + "(%d)"; + } else + nameFormat = tokens[0] + "-%d." + tokens[1]; + + for (int i = 1; ; i++) { + if (!file.isFile()) + break; + + file = new File(dirPath, String.format(nameFormat, i)); + } + + return file.getName(); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/services/DownloadService.java b/app/src/main/java/gr/thmmy/mthmmy/services/DownloadService.java deleted file mode 100644 index a00cffb0..00000000 --- a/app/src/main/java/gr/thmmy/mthmmy/services/DownloadService.java +++ /dev/null @@ -1,228 +0,0 @@ -package gr.thmmy.mthmmy.services; - -import android.app.DownloadManager; -import android.app.IntentService; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Environment; -import android.support.annotation.NonNull; -import android.webkit.MimeTypeMap; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; - -import gr.thmmy.mthmmy.base.BaseApplication; -import gr.thmmy.mthmmy.receiver.Receiver; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okio.BufferedSink; -import okio.Okio; -import timber.log.Timber; - -/** - * An {@link IntentService} subclass for handling asynchronous task requests in - * a service on a separate handler thread. - */ -public class DownloadService extends IntentService { - private static final String TAG = "DownloadService"; - private static int sDownloadId = 0; - - private Receiver receiver; - - public static final String SAVE_DIR = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "mthmmy"; - - public static final String ACTION_DOWNLOAD = "gr.thmmy.mthmmy.services.action.DOWNLOAD"; - public static final String EXTRA_DOWNLOAD_URL = "gr.thmmy.mthmmy.services.extra.DOWNLOAD_URL"; - - public static final String EXTRA_DOWNLOAD_ID = "gr.thmmy.mthmmy.services.extra.DOWNLOAD_ID"; - public static final String EXTRA_DOWNLOAD_STATE = "gr.thmmy.mthmmy.services.extra.DOWNLOAD_STATE"; - public static final String EXTRA_FILE_NAME = "gr.thmmy.mthmmy.services.extra.FILE_NAME"; - public static final String EXTRA_NOTIFICATION_TITLE = "gr.thmmy.mthmmy.services.extra.NOTIFICATION_TITLE"; - public static final String EXTRA_NOTIFICATION_TEXT = "gr.thmmy.mthmmy.services.extra.NOTIFICATION_TEXT"; - public static final String EXTRA_NOTIFICATION_TICKER = "gr.thmmy.mthmmy.services.extra.NOTIFICATION_TICKER"; - - public static final String STARTED = "Started"; - public static final String COMPLETED = "Completed"; - public static final String FAILED = "Failed"; - - - public DownloadService() { - super("DownloadService"); - } - - @Override - public void onCreate() { - super.onCreate(); - final IntentFilter filter = new IntentFilter(DownloadService.ACTION_DOWNLOAD); - receiver = new Receiver(); - registerReceiver(receiver, filter); - - } - - @Override - public void onDestroy() { - super.onDestroy(); - this.unregisterReceiver(receiver); - } - - /** - * Starts this service to perform action Download with the given parameters. If - * the service is already performing a task this action will be queued. - * - * @see IntentService - */ - public static void startActionDownload(Context context, String downloadUrl) { - Intent intent = new Intent(context, DownloadService.class); - intent.setAction(ACTION_DOWNLOAD); - intent.putExtra(EXTRA_DOWNLOAD_URL, downloadUrl); - context.startService(intent); - } - - @Override - protected void onHandleIntent(Intent intent) { - if (intent != null) { - final String action = intent.getAction(); - if (ACTION_DOWNLOAD.equals(action)) { - final String downloadLink = intent.getStringExtra(EXTRA_DOWNLOAD_URL); - handleActionDownload(downloadLink); - } - } - } - - /** - * Handle action Foo in the provided background thread with the provided - * parameters. - */ - private void handleActionDownload(String downloadLink) { - OkHttpClient client = BaseApplication.getInstance().getClient(); - BufferedSink sink = null; - String fileName = "file"; - - int downloadId = sDownloadId; - sDownloadId++; - - try { - Request request = new Request.Builder().url(downloadLink).build(); - Response response = client.newCall(request).execute(); - - String contentDisposition = response.headers("Content-Disposition").toString(); //check if link provides an attachment - if (contentDisposition.contains("attachment")) { - fileName = contentDisposition.split("\"")[1]; - - File dirPath = new File(SAVE_DIR); - if (!dirPath.isDirectory()) { - if (dirPath.mkdirs()) - Timber.i("mTHMMY's directory created successfully!"); - else - Timber.e("Couldn't create mTHMMY's directory..."); - } - - - String nameFormat; - String[] tokens = fileName.split("\\.(?=[^\\.]+$)"); - - if (tokens.length != 2) { - Timber.w("Couldn't get file extension..."); - nameFormat = fileName + "(%d)"; - } else - nameFormat = tokens[0] + "(%d)." + tokens[1]; - - - File file = new File(dirPath, fileName); - - for (int i = 1; ; i++) { - if (!file.exists()) { - break; - } - - file = new File(dirPath, String.format(nameFormat, i)); - } - - fileName = file.getName(); - - Timber.v("Started saving file %s", fileName); - sendNotification(downloadId, STARTED, fileName); - - sink = Okio.buffer(Okio.sink(file)); - sink.writeAll(response.body().source()); - sink.flush(); - Timber.i("Download OK!"); - sendNotification(downloadId, COMPLETED, fileName); - - // Register download - DownloadManager mManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); - long length = file.length(); - mManager.addCompletedDownload(fileName, "edo mporei na mpei ena description", false, getMimeType(file), SAVE_DIR +File.separator+ fileName, length, false); - - } else - Timber.e("No attachment in response!"); - } catch (FileNotFoundException e) { - Timber.i("Download failed..."); - Timber.e(e, "FileNotFound"); - sendNotification(downloadId, FAILED, fileName); - } catch (IOException e) { - Timber.i("Download failed..."); - Timber.e(e, "IOException"); - sendNotification(downloadId, FAILED, fileName); - } finally { - if (sink != null) { - try { - sink.close(); - } catch (IOException e) { - // Ignore - Significant errors should already have been reported - } - } - } - } - - private void sendNotification(int downloadId, String type, @NonNull String fileName) { - Intent intent = new Intent(ACTION_DOWNLOAD); - switch (type) { - case STARTED: { - intent.putExtra(EXTRA_NOTIFICATION_TITLE, "Download Started"); - intent.putExtra(EXTRA_NOTIFICATION_TEXT, "\"" + fileName + "\" downloading..."); - intent.putExtra(EXTRA_NOTIFICATION_TICKER, "Downloading..."); - break; - } - case COMPLETED: { - intent.putExtra(EXTRA_NOTIFICATION_TITLE, "Download Completed"); - intent.putExtra(EXTRA_NOTIFICATION_TEXT, "\"" + fileName + "\" finished downloading."); - intent.putExtra(EXTRA_NOTIFICATION_TICKER, "Download Completed"); - break; - } - case FAILED: { - intent.putExtra(EXTRA_NOTIFICATION_TITLE, "Download Failed"); - intent.putExtra(EXTRA_NOTIFICATION_TEXT, "\"" + fileName + "\" failed."); - intent.putExtra(EXTRA_NOTIFICATION_TICKER, "Download Failed"); - break; - } - default: { - Timber.e("Invalid notification case!"); - return; - } - } - intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId); - intent.putExtra(EXTRA_DOWNLOAD_STATE, type); - intent.putExtra(EXTRA_FILE_NAME, fileName); - sendBroadcast(intent); - - } - - @NonNull - static String getMimeType(@NonNull File file) { - String type = null; - final String url = file.toString(); - final String extension = MimeTypeMap.getFileExtensionFromUrl(url); - if (extension != null) { - type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase()); - } - if (type == null) { - type = ""; // fallback type. You might set it to */* - } - return type; - } - -} diff --git a/app/src/main/java/gr/thmmy/mthmmy/services/NotificationService.java b/app/src/main/java/gr/thmmy/mthmmy/services/NotificationService.java new file mode 100644 index 00000000..ae439dd9 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/services/NotificationService.java @@ -0,0 +1,203 @@ +package gr.thmmy.mthmmy.services; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.service.notification.StatusBarNotification; +import android.support.annotation.RequiresApi; +import android.support.v4.app.NotificationCompat; + +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; + +import org.json.JSONException; +import org.json.JSONObject; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.topic.TopicActivity; +import gr.thmmy.mthmmy.base.BaseApplication; +import gr.thmmy.mthmmy.model.PostNotification; +import timber.log.Timber; + +import static android.support.v4.app.NotificationCompat.PRIORITY_MAX; +import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_TITLE; +import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL; + +public class NotificationService extends FirebaseMessagingService { + private static final int buildVersion = Build.VERSION.SDK_INT; + + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + super.onMessageReceived(remoteMessage); + if (remoteMessage.getData().size() > 0) { + Timber.i("FCM data message received."); + JSONObject json = new JSONObject(remoteMessage.getData()); + try { + int userId = BaseApplication.getInstance().getSessionManager().getUserId(); + //Don't notify me if the sender is me! + if(Integer.parseInt(json.getString("posterId"))!= userId) + { + int topicId = Integer.parseInt(json.getString("topicId")); + int postId = Integer.parseInt(json.getString("postId")); + String topicTitle = json.getString("topicTitle"); + String poster = json.getString("poster"); + sendNotification(new PostNotification(postId, topicId, topicTitle, poster)); + } + else + Timber.v("Notification suppressed (own userID)."); + } catch (JSONException e) { + Timber.e(e, "JSON Exception"); + } + } + } + + private static final String CHANNEL_ID = "Posts"; + private static final String CHANNEL_NAME = "New Posts"; + private static final String GROUP_KEY = "PostsGroup"; + private static int requestCode = 0; + + private static final String NEW_POSTS_COUNT = "newPostsCount"; + public static final String NEW_POST_TAG = "NEW_POST"; + private static final String SUMMARY_TAG = "SUMMARY"; + private static final String DELETED_MESSAGES_TAG = "DELETED_MESSAGES"; + + /** + * Create and show a new post notification. + */ + private void sendNotification(PostNotification postNotification) { + Timber.i("Creating a notification..."); + String topicUrl = "https://www.thmmy.gr/smf/index.php?topic=" + postNotification.getTopicId() + "." + postNotification.getPostId(); + Intent intent = new Intent(this, TopicActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_TOPIC_URL, topicUrl); + extras.putString(BUNDLE_TOPIC_TITLE, postNotification.getTopicTitle()); + intent.putExtras(extras); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + PendingIntent pendingIntent = PendingIntent.getActivity(this, requestCode++, intent, + PendingIntent.FLAG_ONE_SHOT); + + final int topicId = postNotification.getTopicId(); + String contentText = "New post by " + postNotification.getPoster(); + int newPostsCount = 1; + + if (buildVersion >= Build.VERSION_CODES.M){ + Notification existingNotification = getActiveNotification(topicId); + if(existingNotification!=null) + { + newPostsCount = existingNotification.extras.getInt(NEW_POSTS_COUNT) + 1; + contentText = newPostsCount + " new posts"; + } + } + + Bundle notificationExtras = new Bundle(); + notificationExtras.putInt(NEW_POSTS_COUNT, newPostsCount); + + NotificationCompat.Builder notificationBuilder = + new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(postNotification.getTopicTitle()) + .setContentText(contentText) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setDefaults(Notification.DEFAULT_ALL) + .setGroup(GROUP_KEY) + .addExtras(notificationExtras); + + if (buildVersion < Build.VERSION_CODES.O) + notificationBuilder.setPriority(PRIORITY_MAX); + + boolean createSummaryNotification = false; + if(buildVersion >= Build.VERSION_CODES.LOLLIPOP) + { + createSummaryNotification = true; + if(buildVersion >= Build.VERSION_CODES.M) + createSummaryNotification = otherNotificationsExist(topicId); + } + + NotificationCompat.Builder summaryNotificationBuilder = null; + if(createSummaryNotification) + { + summaryNotificationBuilder = + new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setGroupSummary(true) + .setGroup(GROUP_KEY) + .setAutoCancel(true) + .setStyle(new NotificationCompat.InboxStyle() + .setSummaryText("New Posts")) + .setDefaults(Notification.DEFAULT_ALL); + } + + + + + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + // Since Android Oreo notification channel is needed. + if (buildVersion >= Build.VERSION_CODES.O) + notificationManager.createNotificationChannel(new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH)); + + notificationManager.notify(NEW_POST_TAG, topicId, notificationBuilder.build()); + + if(createSummaryNotification) + notificationManager.notify(SUMMARY_TAG,0, summaryNotificationBuilder.build()); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + private Notification getActiveNotification(int notificationId) { + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if(notificationManager!=null) + { + StatusBarNotification[] barNotifications = notificationManager.getActiveNotifications(); + for(StatusBarNotification notification: barNotifications) { + if (notification.getId() == notificationId) + return notification.getNotification(); + } + + } + return null; + } + + @RequiresApi(api = Build.VERSION_CODES.M) + private boolean otherNotificationsExist(int notificationId){ + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if(notificationManager!=null) { + StatusBarNotification[] barNotifications = notificationManager.getActiveNotifications(); + for (StatusBarNotification notification : barNotifications) { + String tag = notification.getTag(); + if (tag!=null && tag.equals(NEW_POST_TAG) && notification.getId() != notificationId) + return true; + } + } + return false; + } + + @Override + public void onDeletedMessages() { + super.onDeletedMessages(); + NotificationCompat.Builder notificationBuilder = + new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle("Error fetching notifications!") + .setContentText("Some notifications may not have arrived successfully either due to" + + "the amount of pending messages (>100) or if the device hasn't come online for more than a month.") + .setAutoCancel(true) + .setDefaults(Notification.DEFAULT_ALL); + + if (buildVersion < Build.VERSION_CODES.O) + notificationBuilder.setPriority(Notification.PRIORITY_MAX); + + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + // Since Android Oreo notification channel is needed. + if (buildVersion >= Build.VERSION_CODES.O) + notificationManager.createNotificationChannel(new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH)); + + notificationManager.notify(DELETED_MESSAGES_TAG, 0, notificationBuilder.build()); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java b/app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java index 4c2c60cc..9168ebf3 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java +++ b/app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java @@ -17,7 +17,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import gr.thmmy.mthmmy.utils.exceptions.ParseException; +import gr.thmmy.mthmmy.utils.parsing.ParseException; import okhttp3.Cookie; import okhttp3.FormBody; import okhttp3.HttpUrl; @@ -47,6 +47,7 @@ public class SessionManager { public static final int CANCELLED = 4; public static final int CONNECTION_ERROR = 5; public static final int EXCEPTION = 6; + public static final int BANNED_USER = 7; // Client & Cookies private final OkHttpClient client; @@ -56,6 +57,7 @@ public class SessionManager { //Shared Preferences & its keys private final SharedPreferences sharedPrefs; private static final String USERNAME = "Username"; + private static final String USER_ID = "UserID"; private static final String AVATAR_LINK = "AvatarLink"; private static final String HAS_AVATAR = "HasAvatar"; private static final String LOGOUT_LINK = "LogoutLink"; @@ -108,17 +110,16 @@ public class SessionManager { Response response = client.newCall(request).execute(); Document document = Jsoup.parse(response.body().string()); - Elements unreadRepliesLinks = document.select("a[href=https://www.thmmy.gr/smf/index.php?action=unreadreplies]"); - - if (unreadRepliesLinks.size() >= 2) //Normally it's just == 2, but who knows what can be posted by users + if (validateRetrievedCookies()) { Timber.i("Login successful!"); setPersistentCookieSession(); //Store cookies //Edit SharedPreferences, save session's data + setLoginScreenAsDefault(false); sharedPrefs.edit().putBoolean(LOGGED_IN, true).apply(); - sharedPrefs.edit().putBoolean(LOGIN_SCREEN_AS_DEFAULT, false).apply(); sharedPrefs.edit().putString(USERNAME, extractUserName(document)).apply(); + sharedPrefs.edit().putInt(USER_ID, extractUserId(document)).apply(); String avatar = extractAvatarLink(document); if (avatar != null) { sharedPrefs.edit().putBoolean(HAS_AVATAR, true).apply(); @@ -135,17 +136,23 @@ public class SessionManager { //Investigate login failure Elements error = document.select("b:contains(That username does not exist.)"); - if (error.size() == 1) { //Wrong username + if (error.size() > 0) { //Wrong username Timber.i("Wrong Username"); return WRONG_USER; } error = document.select("body:contains(Password incorrect)"); - if (error.size() == 1) { //Wrong password + if (error.size() > 0) { //Wrong password Timber.i("Wrong Password"); return WRONG_PASSWORD; } + error = document.select("body:contains(you are banned from using this forum!),body:contains(έχετε αποκλειστεί από αυτή τη δημόσια συζήτηση!)"); + if (error.size() > 0) { //User is banned + Timber.i("User is banned"); + return BANNED_USER; + } + //Other error e.g. session was reset server-side clearSessionData(); //Clear invalid saved data return FAILURE; @@ -182,7 +189,7 @@ public class SessionManager { } else if (isLoginScreenDefault()) return; - sharedPrefs.edit().putBoolean(LOGIN_SCREEN_AS_DEFAULT, true).apply(); + setLoginScreenAsDefault(true); clearSessionData(); } @@ -192,7 +199,7 @@ public class SessionManager { public void guestLogin() { Timber.i("Continuing as a guest, as chosen by the user."); clearSessionData(); - sharedPrefs.edit().putBoolean(LOGIN_SCREEN_AS_DEFAULT, false).apply(); + setLoginScreenAsDefault(false); } @@ -221,7 +228,7 @@ public class SessionManager { return FAILURE; } } catch (IOException e) { - Timber.w("Logout IOException", e); + Timber.w(e, "Logout IOException"); return CONNECTION_ERROR; } catch (Exception e) { Timber.e(e, "Logout Exception"); @@ -236,11 +243,25 @@ public class SessionManager { //---------------------------------------GETTERS------------------------------------------------ public String getUsername() { - return sharedPrefs.getString(USERNAME, "Username"); + return sharedPrefs.getString(USERNAME, USERNAME); + } + + public int getUserId() { + return sharedPrefs.getInt(USER_ID, -1); } public String getAvatarLink() { - return sharedPrefs.getString(AVATAR_LINK, "AvatarLink"); + return sharedPrefs.getString(AVATAR_LINK, AVATAR_LINK); + } + + public Cookie getThmmyCookie() { + List