From df5a96e035ed86beb51658879a9cc68c13782a7b Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sun, 14 Mar 2021 00:21:31 +0900 Subject: [PATCH] DM sync service --- app/build.gradle | 3 +- .../awais.instagrabber.db.AppDatabase/5.json | 161 ++++++++ app/src/main/AndroidManifest.xml | 14 +- .../instagrabber/activities/MainActivity.java | 93 ++++- .../DirectInboxItemViewHolder.java | 232 +---------- .../directmessages/DirectItemViewHolder.java | 11 +- .../awais/instagrabber/db/AppDatabase.java | 22 +- .../awais/instagrabber/db/Converters.java | 16 + .../db/dao/DMLastNotifiedDao.java | 34 ++ .../datasources/DMLastNotifiedDataSource.java | 70 ++++ .../db/entities/DMLastNotified.java | 85 ++++ .../DMLastNotifiedRepository.java | 126 ++++++ .../settings/DMPreferencesFragment.java | 201 ++++++++++ .../DownloadsPreferencesFragment.java | 101 +++++ .../settings/GeneralPreferencesFragment.java | 61 +++ .../settings/LocalePreferencesFragment.java | 48 +++ .../NotificationsPreferencesFragment.java | 43 ++ .../settings/PostPreferencesFragment.java | 94 +++++ .../fragments/settings/PreferenceHelper.java | 30 ++ .../fragments/settings/PreferenceKeys.java | 8 + .../settings/SettingsPreferencesFragment.java | 366 +++--------------- .../settings/StoriesPreferencesFragment.java | 48 +++ .../instagrabber/managers/InboxManager.java | 2 + .../instagrabber/managers/ThreadManager.java | 2 + .../responses/directmessages/DirectItem.java | 11 + .../services/ActivityCheckerService.java | 2 +- .../services/DMSyncAlarmReceiver.java | 87 +++++ .../instagrabber/services/DMSyncService.java | 248 ++++++++++++ .../awais/instagrabber/utils/Constants.java | 31 +- .../awais/instagrabber/utils/DMUtils.java | 273 +++++++++++++ .../awais/instagrabber/utils/DateUtils.java | 5 + .../awais/instagrabber/utils/FlavorTown.java | 4 +- .../instagrabber/utils/ResponseBodyUtils.java | 22 -- .../instagrabber/utils/SettingsHelper.java | 10 +- .../res/drawable/ic_round_mode_comment_24.xml | 10 + .../res/layout/pref_auto_refresh_dm_freq.xml | 33 ++ .../main/res/navigation/more_nav_graph.xml | 51 ++- app/src/main/res/values/arrays.xml | 8 + app/src/main/res/values/strings.xml | 10 + 39 files changed, 2076 insertions(+), 600 deletions(-) create mode 100644 app/schemas/awais.instagrabber.db.AppDatabase/5.json create mode 100644 app/src/main/java/awais/instagrabber/db/dao/DMLastNotifiedDao.java create mode 100644 app/src/main/java/awais/instagrabber/db/datasources/DMLastNotifiedDataSource.java create mode 100644 app/src/main/java/awais/instagrabber/db/entities/DMLastNotified.java create mode 100644 app/src/main/java/awais/instagrabber/db/repositories/DMLastNotifiedRepository.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/settings/DMPreferencesFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/settings/GeneralPreferencesFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/settings/LocalePreferencesFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/settings/NotificationsPreferencesFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/settings/PreferenceHelper.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/settings/StoriesPreferencesFragment.java create mode 100644 app/src/main/java/awais/instagrabber/services/DMSyncAlarmReceiver.java create mode 100644 app/src/main/java/awais/instagrabber/services/DMSyncService.java create mode 100644 app/src/main/java/awais/instagrabber/utils/DMUtils.java create mode 100644 app/src/main/res/drawable/ic_round_mode_comment_24.xml create mode 100644 app/src/main/res/layout/pref_auto_refresh_dm_freq.xml diff --git a/app/build.gradle b/app/build.gradle index 222f72ba..ec1da5b4 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -79,6 +79,7 @@ dependencies { implementation "androidx.preference:preference:1.1.1" implementation "androidx.work:work-runtime:2.5.0" implementation 'androidx.palette:palette:1.0.0' + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation 'com.google.guava:guava:27.0.1-android' @@ -89,7 +90,7 @@ dependencies { annotationProcessor "androidx.room:room-compiler:$room_version" // CameraX - def camerax_version = "1.0.0-alpha02" + def camerax_version = "1.1.0-alpha02" implementation "androidx.camera:camera-camera2:$camerax_version" implementation "androidx.camera:camera-lifecycle:$camerax_version" implementation "androidx.camera:camera-view:1.0.0-alpha22" diff --git a/app/schemas/awais.instagrabber.db.AppDatabase/5.json b/app/schemas/awais.instagrabber.db.AppDatabase/5.json new file mode 100644 index 00000000..a60a7ea8 --- /dev/null +++ b/app/schemas/awais.instagrabber.db.AppDatabase/5.json @@ -0,0 +1,161 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "0b38e12b76bb081ec837191c5ef5b54e", + "entities": [ + { + "tableName": "accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `username` TEXT, `cookie` TEXT, `full_name` TEXT, `profile_pic` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "profilePic", + "columnName": "profile_pic", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query_text` TEXT, `type` TEXT, `display_name` TEXT, `pic_url` TEXT, `date_added` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query_text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "picUrl", + "columnName": "pic_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateAdded", + "columnName": "date_added", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dm_last_notified", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thread_id` TEXT, `last_notified_msg_ts` INTEGER, `last_notified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "threadId", + "columnName": "thread_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastNotifiedMsgTs", + "columnName": "last_notified_msg_ts", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastNotifiedAt", + "columnName": "last_notified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_dm_last_notified_thread_id", + "unique": true, + "columnNames": [ + "thread_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_last_notified_thread_id` ON `${TABLE_NAME}` (`thread_id`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0b38e12b76bb081ec837191c5ef5b54e')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ba525c14..403ffd78 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,7 +13,6 @@ android:name="android.hardware.camera.any" android:required="false" /> - - - + + @@ -147,6 +146,14 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java index 341fe21c..6abc076e 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java @@ -38,6 +38,7 @@ import androidx.emoji.text.FontRequestEmojiCompatConfig; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.NavBackStackEntry; import androidx.navigation.NavController; @@ -62,12 +63,15 @@ import awais.instagrabber.asyncs.SuggestionsFetcher; import awais.instagrabber.customviews.emoji.EmojiVariantManager; import awais.instagrabber.databinding.ActivityMainBinding; import awais.instagrabber.fragments.PostViewV2Fragment; +import awais.instagrabber.fragments.directmessages.DirectMessageInboxFragmentDirections; import awais.instagrabber.fragments.main.FeedFragment; +import awais.instagrabber.fragments.settings.PreferenceKeys; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.IntentModel; import awais.instagrabber.models.SuggestionModel; import awais.instagrabber.models.enums.SuggestionType; import awais.instagrabber.services.ActivityCheckerService; +import awais.instagrabber.services.DMSyncAlarmReceiver; import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; @@ -159,6 +163,14 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage EmojiVariantManager.getInstance(); }); initEmojiCompat(); + initDmService(); + } + + private void initDmService() { + if (!isLoggedIn) return; + final boolean enabled = settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH); + if (!enabled) return; + DMSyncAlarmReceiver.setAlarm(this); } @Override @@ -247,15 +259,22 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage } private void createNotificationChannels() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); - notificationManager.createNotificationChannel(new NotificationChannel(Constants.DOWNLOAD_CHANNEL_ID, - Constants.DOWNLOAD_CHANNEL_NAME, - NotificationManager.IMPORTANCE_DEFAULT)); - notificationManager.createNotificationChannel(new NotificationChannel(Constants.ACTIVITY_CHANNEL_ID, - Constants.ACTIVITY_CHANNEL_NAME, - NotificationManager.IMPORTANCE_DEFAULT)); - } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; + final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); + notificationManager.createNotificationChannel(new NotificationChannel(Constants.DOWNLOAD_CHANNEL_ID, + Constants.DOWNLOAD_CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT)); + notificationManager.createNotificationChannel(new NotificationChannel(Constants.ACTIVITY_CHANNEL_ID, + Constants.ACTIVITY_CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT)); + notificationManager.createNotificationChannel(new NotificationChannel(Constants.DM_UNREAD_CHANNEL_ID, + Constants.DM_UNREAD_CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT)); + final NotificationChannel silentNotificationChannel = new NotificationChannel(Constants.SILENT_NOTIFICATIONS_CHANNEL_ID, + Constants.SILENT_NOTIFICATIONS_CHANNEL_NAME, + NotificationManager.IMPORTANCE_LOW); + silentNotificationChannel.setSound(null, null); + notificationManager.createNotificationChannel(silentNotificationChannel); } private void setupSuggestions() { @@ -521,6 +540,10 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage showActivityView(); return; } + if (Constants.ACTION_SHOW_DM_THREAD.equals(action)) { + showThread(intent); + return; + } if (Intent.ACTION_SEND.equals(action) && type != null) { if (type.equals("text/plain")) { handleUrl(intent.getStringExtra(Intent.EXTRA_TEXT)); @@ -534,6 +557,58 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage } } + private void showThread(@NonNull final Intent intent) { + final String threadId = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_ID); + final String threadTitle = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_TITLE); + navigateToThread(threadId, threadTitle); + } + + public void navigateToThread(final String threadId, final String threadTitle) { + if (threadId == null || threadTitle == null) return; + currentNavControllerLiveData.observe(this, new Observer() { + @Override + public void onChanged(final NavController navController) { + if (navController == null) return; + if (navController.getGraph().getId() != R.id.direct_messages_nav_graph) return; + try { + final NavDestination currentDestination = navController.getCurrentDestination(); + if (currentDestination != null && currentDestination.getId() == R.id.directMessagesInboxFragment) { + // if we are already on the inbox page, navigate to the thread + // need handler.post() to wait for the fragment manager to be ready to navigate + new Handler().post(() -> { + final DirectMessageInboxFragmentDirections.ActionInboxToThread action = DirectMessageInboxFragmentDirections + .actionInboxToThread(threadId, threadTitle); + navController.navigate(action); + }); + return; + } + // add a destination change listener to navigate to thread once we are on the inbox page + navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() { + @Override + public void onDestinationChanged(@NonNull final NavController controller, + @NonNull final NavDestination destination, + @Nullable final Bundle arguments) { + if (destination.getId() == R.id.directMessagesInboxFragment) { + final DirectMessageInboxFragmentDirections.ActionInboxToThread action = DirectMessageInboxFragmentDirections + .actionInboxToThread(threadId, threadTitle); + controller.navigate(action); + controller.removeOnDestinationChangedListener(this); + } + } + }); + // pop back stack until we reach the inbox page + navController.popBackStack(R.id.directMessagesInboxFragment, false); + } finally { + currentNavControllerLiveData.removeObserver(this); + } + } + }); + final int selectedItemId = binding.bottomNavView.getSelectedItemId(); + if (selectedItemId != R.navigation.direct_messages_nav_graph) { + setBottomNavSelectedItem(R.navigation.direct_messages_nav_graph); + } + } + private void handleUrl(final String url) { if (url == null) return; // Log.d(TAG, url); diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java index df820e8b..41465b38 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java @@ -12,26 +12,17 @@ import androidx.recyclerview.widget.RecyclerView; import com.facebook.drawee.view.SimpleDraweeView; import com.google.common.collect.ImmutableList; -import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import awais.instagrabber.R; import awais.instagrabber.adapters.DirectMessageInboxAdapter.OnItemClickListener; import awais.instagrabber.databinding.LayoutDmInboxItemBinding; -import awais.instagrabber.models.enums.DirectItemType; import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.directmessages.DirectItem; -import awais.instagrabber.repositories.responses.directmessages.DirectItemReelShare; -import awais.instagrabber.repositories.responses.directmessages.DirectItemVisualMedia; import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.DirectThreadDirectStory; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadLastSeenAt; -import awais.instagrabber.repositories.responses.directmessages.RavenExpiringMediaActionSummary; -import awais.instagrabber.utils.ResponseBodyUtils; +import awais.instagrabber.utils.DMUtils; import awais.instagrabber.utils.TextUtils; public final class DirectInboxItemViewHolder extends RecyclerView.ViewHolder { @@ -133,218 +124,17 @@ public final class DirectInboxItemViewHolder extends RecyclerView.ViewHolder { if (directStory != null && !directStory.getItems().isEmpty()) { final DirectItem item = directStory.getItems().get(0); final MediaItemType mediaType = item.getVisualMedia().getMedia().getMediaType(); - final String username = getUsername(thread.getUsers(), item.getUserId(), viewerId, resources); - final String subtitle = getMediaSpecificSubtitle(username, resources, mediaType); + final String username = DMUtils.getUsername(thread.getUsers(), item.getUserId(), viewerId, resources); + final String subtitle = DMUtils.getMediaSpecificSubtitle(username, resources, mediaType); binding.subtitle.setText(subtitle); return; } final DirectItem item = thread.getFirstDirectItem(); if (item == null) return; - final long senderId = item.getUserId(); - final DirectItemType itemType = item.getItemType(); - String subtitle = null; - final String username = getUsername(thread.getUsers(), senderId, viewerId, resources); - String message = ""; - if (itemType == null) { - message = resources.getString(R.string.dms_inbox_raven_message_unknown); - } else { - switch (itemType) { - case TEXT: - message = item.getText(); - break; - case LIKE: - message = item.getLike(); - break; - case LINK: - message = item.getLink().getText(); - break; - case PLACEHOLDER: - message = item.getPlaceholder().getMessage(); - break; - case MEDIA_SHARE: - subtitle = resources.getString(R.string.dms_inbox_shared_post, username != null ? username : "", item.getMediaShare().getUser().getUsername()); - break; - case ANIMATED_MEDIA: - subtitle = resources.getString(R.string.dms_inbox_shared_gif, username != null ? username : ""); - break; - case PROFILE: - subtitle = resources.getString(R.string.dms_inbox_shared_profile, username != null ? username : "", item.getProfile().getUsername()); - break; - case LOCATION: - subtitle = resources.getString(R.string.dms_inbox_shared_location, username != null ? username : "", item.getLocation().getName()); - break; - case MEDIA: { - final MediaItemType mediaType = item.getMedia().getMediaType(); - subtitle = getMediaSpecificSubtitle(username, resources, mediaType); - break; - } - case STORY_SHARE: { - final String reelType = item.getStoryShare().getReelType(); - if (reelType == null) { - subtitle = item.getStoryShare().getTitle(); - } else { - final int format = reelType.equals("highlight_reel") - ? R.string.dms_inbox_shared_highlight - : R.string.dms_inbox_shared_story; - subtitle = resources.getString(format, username != null ? username : "", - item.getStoryShare().getMedia().getUser().getUsername()); - } - break; - } - case VOICE_MEDIA: - subtitle = resources.getString(R.string.dms_inbox_shared_voice, username != null ? username : ""); - break; - case ACTION_LOG: - subtitle = item.getActionLog().getDescription(); - break; - case VIDEO_CALL_EVENT: - subtitle = item.getVideoCallEvent().getDescription(); - break; - case CLIP: - subtitle = resources.getString(R.string.dms_inbox_shared_clip, username != null ? username : "", - item.getClip().getClip().getUser().getUsername()); - break; - case FELIX_SHARE: - subtitle = resources.getString(R.string.dms_inbox_shared_igtv, username != null ? username : "", - item.getFelixShare().getVideo().getUser().getUsername()); - break; - case RAVEN_MEDIA: - subtitle = getRavenMediaSubtitle(item, resources, username); - break; - case REEL_SHARE: - final DirectItemReelShare reelShare = item.getReelShare(); - if (reelShare == null) { - subtitle = ""; - break; - } - final String reelType = reelShare.getType(); - switch (reelType) { - case "reply": - if (viewerId == item.getUserId()) { - subtitle = resources.getString(R.string.dms_inbox_replied_story_outgoing, reelShare.getText()); - } else { - subtitle = resources.getString(R.string.dms_inbox_replied_story_incoming, username != null ? username : "", reelShare.getText()); - } - break; - case "mention": - if (viewerId == item.getUserId()) { - // You mentioned the other person - final long mentionedUserId = item.getReelShare().getMentionedUserId(); - final String otherUsername = getUsername(thread.getUsers(), mentionedUserId, viewerId, resources); - subtitle = resources.getString(R.string.dms_inbox_mentioned_story_outgoing, otherUsername); - } else { - // They mentioned you - subtitle = resources.getString(R.string.dms_inbox_mentioned_story_incoming, username != null ? username : ""); - } - break; - case "reaction": - if (viewerId == item.getUserId()) { - subtitle = resources.getString(R.string.dms_inbox_reacted_story_outgoing, reelShare.getText()); - } else { - subtitle = resources.getString(R.string.dms_inbox_reacted_story_incoming, username != null ? username : "", reelShare.getText()); - } - break; - default: - subtitle = ""; - break; - } - break; - default: - message = resources.getString(R.string.dms_inbox_raven_message_unknown); - } - } - if (subtitle == null) { - if (thread.getUsers().size() > 1 - || (thread.getUsers().size() == 1 && senderId == viewerId)) { - subtitle = String.format("%s: %s", username != null ? username : "", message); - } else { - subtitle = message; - } - } + final String subtitle = DMUtils.getMessageString(thread, resources, viewerId, item); binding.subtitle.setText(subtitle != null ? subtitle : ""); } - private String getMediaSpecificSubtitle(final String username, final Resources resources, final MediaItemType mediaType) { - final String userSharedAnImage = resources.getString(R.string.dms_inbox_shared_image, username != null ? username : ""); - final String userSharedAVideo = resources.getString(R.string.dms_inbox_shared_video, username != null ? username : ""); - final String userSentAMessage = resources.getString(R.string.dms_inbox_shared_message, username != null ? username : ""); - String subtitle; - switch (mediaType) { - case MEDIA_TYPE_IMAGE: - subtitle = userSharedAnImage; - break; - case MEDIA_TYPE_VIDEO: - subtitle = userSharedAVideo; - break; - default: - subtitle = userSentAMessage; - break; - } - return subtitle; - } - - private String getRavenMediaSubtitle(final DirectItem item, - final Resources resources, - final String username) { - String subtitle = "↗ "; - final DirectItemVisualMedia visualMedia = item.getVisualMedia(); - final RavenExpiringMediaActionSummary summary = visualMedia.getExpiringMediaActionSummary(); - if (summary != null) { - final RavenExpiringMediaActionSummary.ActionType expiringMediaType = summary.getType(); - int textRes = 0; - switch (expiringMediaType) { - case DELIVERED: - textRes = R.string.dms_inbox_raven_media_delivered; - break; - case SENT: - textRes = R.string.dms_inbox_raven_media_sent; - break; - case OPENED: - textRes = R.string.dms_inbox_raven_media_opened; - break; - case REPLAYED: - textRes = R.string.dms_inbox_raven_media_replayed; - break; - case SENDING: - textRes = R.string.dms_inbox_raven_media_sending; - break; - case BLOCKED: - textRes = R.string.dms_inbox_raven_media_blocked; - break; - case SUGGESTED: - textRes = R.string.dms_inbox_raven_media_suggested; - break; - case SCREENSHOT: - textRes = R.string.dms_inbox_raven_media_screenshot; - break; - case CANNOT_DELIVER: - textRes = R.string.dms_inbox_raven_media_cant_deliver; - break; - } - if (textRes > 0) { - subtitle += itemView.getContext().getString(textRes); - } - return subtitle; - } - final MediaItemType mediaType = visualMedia.getMedia().getMediaType(); - subtitle = getMediaSpecificSubtitle(username, resources, mediaType); - return subtitle; - } - - private String getUsername(final List users, - final long userId, - final long viewerId, - final Resources resources) { - if (userId == viewerId) { - return resources.getString(R.string.you); - } - final Optional senderOptional = users.stream() - .filter(Objects::nonNull) - .filter(user -> user.getPk() == userId) - .findFirst(); - return senderOptional.map(User::getUsername).orElse(null); - } - private void setDateTime(@NonNull final DirectItem item) { final long timestamp = item.getTimestamp() / 1000; final String dateTimeString = TextUtils.getRelativeDateTimeString(itemView.getContext(), timestamp); @@ -352,19 +142,7 @@ public final class DirectInboxItemViewHolder extends RecyclerView.ViewHolder { } private void setReadState(@NonNull final DirectThread thread) { - final boolean read; - if (thread.getDirectStory() != null) { - read = false; - } else { - final DirectItem item = thread.getFirstDirectItem(); - if (item.getUserId() == thread.getViewerId()) { - // if last item was sent by user, then it is read (even though we have auto read unchecked?) - read = true; - } else { - final Map lastSeenAtMap = thread.getLastSeenAt(); - read = ResponseBodyUtils.isRead(item, lastSeenAtMap, Collections.singletonList(thread.getViewerId())); - } - } + final boolean read = DMUtils.isRead(thread); binding.unread.setVisibility(read ? View.GONE : View.VISIBLE); binding.threadTitle.setTypeface(binding.threadTitle.getTypeface(), read ? Typeface.NORMAL : Typeface.BOLD); binding.subtitle.setTypeface(binding.subtitle.getTypeface(), read ? Typeface.NORMAL : Typeface.BOLD); diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java index d194d718..8271c38c 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java @@ -45,6 +45,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiR import awais.instagrabber.repositories.responses.directmessages.DirectItemReactions; import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare; import awais.instagrabber.repositories.responses.directmessages.DirectThread; +import awais.instagrabber.utils.DMUtils; import awais.instagrabber.utils.DeepLinkParser; import awais.instagrabber.utils.ResponseBodyUtils; @@ -196,9 +197,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple if (item.isPending()) { binding.deliveryStatus.setImageResource(R.drawable.ic_check_24); } else { - final boolean read = ResponseBodyUtils.isRead(item, - thread.getLastSeenAt(), - userIds + final boolean read = DMUtils.isRead(item, + thread.getLastSeenAt(), + userIds ); binding.deliveryStatus.setImageResource(R.drawable.ic_check_all_24); ImageViewCompat.setImageTintList( @@ -324,8 +325,8 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple final String repliedToUsername = user != null ? user.getUsername() : ""; if (item.getUserId() == currentUser.getPk()) { return thread.isGroup() - ? resources.getString(R.string.replied_you_group, repliedToUsername) - : resources.getString(R.string.replied_you); + ? resources.getString(R.string.replied_you_group, repliedToUsername) + : resources.getString(R.string.replied_you); } if (repliedToUserId == currentUser.getPk()) { return resources.getString(R.string.replied_to_you); diff --git a/app/src/main/java/awais/instagrabber/db/AppDatabase.java b/app/src/main/java/awais/instagrabber/db/AppDatabase.java index 11547601..63e2ce43 100644 --- a/app/src/main/java/awais/instagrabber/db/AppDatabase.java +++ b/app/src/main/java/awais/instagrabber/db/AppDatabase.java @@ -20,14 +20,16 @@ import java.util.Date; import java.util.List; import awais.instagrabber.db.dao.AccountDao; +import awais.instagrabber.db.dao.DMLastNotifiedDao; import awais.instagrabber.db.dao.FavoriteDao; import awais.instagrabber.db.entities.Account; +import awais.instagrabber.db.entities.DMLastNotified; import awais.instagrabber.db.entities.Favorite; import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.utils.Utils; -@Database(entities = {Account.class, Favorite.class}, - version = 4) +@Database(entities = {Account.class, Favorite.class, DMLastNotified.class}, + version = 5) @TypeConverters({Converters.class}) public abstract class AppDatabase extends RoomDatabase { private static final String TAG = AppDatabase.class.getSimpleName(); @@ -38,12 +40,14 @@ public abstract class AppDatabase extends RoomDatabase { public abstract FavoriteDao favoriteDao(); + public abstract DMLastNotifiedDao dmLastNotifiedDao(); + public static AppDatabase getDatabase(final Context context) { if (INSTANCE == null) { synchronized (AppDatabase.class) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "cookiebox.db") - .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) .build(); } } @@ -140,6 +144,18 @@ public abstract class AppDatabase extends RoomDatabase { } }; + static final Migration MIGRATION_4_5 = new Migration(4, 5) { + @Override + public void migrate(@NonNull final SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `dm_last_notified` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`thread_id` TEXT, " + + "`last_notified_msg_ts` INTEGER, " + + "`last_notified_at` INTEGER)"); + database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_last_notified_thread_id` ON `dm_last_notified` (`thread_id`)"); + } + }; + @NonNull private static List backupOldFavorites(@NonNull final SupportSQLiteDatabase db) { // check if old favorites table had the column query_display diff --git a/app/src/main/java/awais/instagrabber/db/Converters.java b/app/src/main/java/awais/instagrabber/db/Converters.java index 50252755..de10e3fc 100644 --- a/app/src/main/java/awais/instagrabber/db/Converters.java +++ b/app/src/main/java/awais/instagrabber/db/Converters.java @@ -2,6 +2,10 @@ package awais.instagrabber.db; import androidx.room.TypeConverter; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.Date; import awais.instagrabber.models.enums.FavoriteType; @@ -30,4 +34,16 @@ public class Converters { public static String favoriteTypeToString(FavoriteType favoriteType) { return favoriteType == null ? null : favoriteType.toString(); } + + @TypeConverter + public static LocalDateTime fromTimestampToLocalDateTime(Long value) { + if (value == null) return null; + return LocalDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneOffset.systemDefault()); + } + + @TypeConverter + public static Long localDateTimeToTimestamp(LocalDateTime localDateTime) { + if (localDateTime == null) return null; + return localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } } diff --git a/app/src/main/java/awais/instagrabber/db/dao/DMLastNotifiedDao.java b/app/src/main/java/awais/instagrabber/db/dao/DMLastNotifiedDao.java new file mode 100644 index 00000000..ddd6968c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/dao/DMLastNotifiedDao.java @@ -0,0 +1,34 @@ +package awais.instagrabber.db.dao; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; +import androidx.room.Update; + +import java.util.List; + +import awais.instagrabber.db.entities.DMLastNotified; + +@Dao +public interface DMLastNotifiedDao { + + @Query("SELECT * FROM dm_last_notified") + List getAllDMDmLastNotified(); + + @Query("SELECT * FROM dm_last_notified WHERE thread_id = :threadId") + DMLastNotified findDMLastNotifiedByThreadId(String threadId); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + List insertDMLastNotified(DMLastNotified... dmLastNotified); + + @Update + void updateDMLastNotified(DMLastNotified... dmLastNotified); + + @Delete + void deleteDMLastNotified(DMLastNotified... dmLastNotified); + + @Query("DELETE from dm_last_notified") + void deleteAllDMLastNotified(); +} diff --git a/app/src/main/java/awais/instagrabber/db/datasources/DMLastNotifiedDataSource.java b/app/src/main/java/awais/instagrabber/db/datasources/DMLastNotifiedDataSource.java new file mode 100644 index 00000000..70a9a171 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/datasources/DMLastNotifiedDataSource.java @@ -0,0 +1,70 @@ +package awais.instagrabber.db.datasources; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.time.LocalDateTime; +import java.util.List; + +import awais.instagrabber.db.AppDatabase; +import awais.instagrabber.db.dao.DMLastNotifiedDao; +import awais.instagrabber.db.entities.DMLastNotified; + +public class DMLastNotifiedDataSource { + private static final String TAG = DMLastNotifiedDataSource.class.getSimpleName(); + + private static DMLastNotifiedDataSource INSTANCE; + + private final DMLastNotifiedDao dmLastNotifiedDao; + + private DMLastNotifiedDataSource(final DMLastNotifiedDao dmLastNotifiedDao) { + this.dmLastNotifiedDao = dmLastNotifiedDao; + } + + public static DMLastNotifiedDataSource getInstance(@NonNull Context context) { + if (INSTANCE == null) { + synchronized (DMLastNotifiedDataSource.class) { + if (INSTANCE == null) { + final AppDatabase database = AppDatabase.getDatabase(context); + INSTANCE = new DMLastNotifiedDataSource(database.dmLastNotifiedDao()); + } + } + } + return INSTANCE; + } + + @Nullable + public final DMLastNotified getDMLastNotified(final String threadId) { + return dmLastNotifiedDao.findDMLastNotifiedByThreadId(threadId); + } + + @NonNull + public final List getAllDMDmLastNotified() { + return dmLastNotifiedDao.getAllDMDmLastNotified(); + } + + public final void insertOrUpdateDMLastNotified(final String threadId, + final LocalDateTime lastNotifiedMsgTs, + final LocalDateTime lastNotifiedAt) { + final DMLastNotified dmLastNotified = getDMLastNotified(threadId); + final DMLastNotified toUpdate = new DMLastNotified(dmLastNotified == null ? 0 : dmLastNotified.getId(), + threadId, + lastNotifiedMsgTs, + lastNotifiedAt); + if (dmLastNotified != null) { + dmLastNotifiedDao.updateDMLastNotified(toUpdate); + return; + } + dmLastNotifiedDao.insertDMLastNotified(toUpdate); + } + + public final void deleteDMLastNotified(@NonNull final DMLastNotified dmLastNotified) { + dmLastNotifiedDao.deleteDMLastNotified(dmLastNotified); + } + + public final void deleteAllDMLastNotified() { + dmLastNotifiedDao.deleteAllDMLastNotified(); + } +} diff --git a/app/src/main/java/awais/instagrabber/db/entities/DMLastNotified.java b/app/src/main/java/awais/instagrabber/db/entities/DMLastNotified.java new file mode 100644 index 00000000..c0ae776f --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/entities/DMLastNotified.java @@ -0,0 +1,85 @@ +package awais.instagrabber.db.entities; + +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.Index; +import androidx.room.PrimaryKey; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity(tableName = DMLastNotified.TABLE_NAME, indices = {@Index(value = DMLastNotified.COL_THREAD_ID, unique = true)}) +public class DMLastNotified { + public final static String TABLE_NAME = "dm_last_notified"; + public final static String COL_ID = "id"; + public final static String COL_THREAD_ID = "thread_id"; + public final static String COL_LAST_NOTIFIED_MSG_TS = "last_notified_msg_ts"; + public final static String COL_LAST_NOTIFIED_AT = "last_notified_at"; + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + private final int id; + + @ColumnInfo(name = COL_THREAD_ID) + private final String threadId; + + @ColumnInfo(name = COL_LAST_NOTIFIED_MSG_TS) + private final LocalDateTime lastNotifiedMsgTs; + + @ColumnInfo(name = COL_LAST_NOTIFIED_AT) + private final LocalDateTime lastNotifiedAt; + + public DMLastNotified(final int id, + final String threadId, + final LocalDateTime lastNotifiedMsgTs, + final LocalDateTime lastNotifiedAt) { + this.id = id; + this.threadId = threadId; + this.lastNotifiedMsgTs = lastNotifiedMsgTs; + this.lastNotifiedAt = lastNotifiedAt; + } + + public int getId() { + return id; + } + + public String getThreadId() { + return threadId; + } + + public LocalDateTime getLastNotifiedMsgTs() { + return lastNotifiedMsgTs; + } + + public LocalDateTime getLastNotifiedAt() { + return lastNotifiedAt; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DMLastNotified that = (DMLastNotified) o; + return id == that.id && + Objects.equals(threadId, that.threadId) && + Objects.equals(lastNotifiedMsgTs, that.lastNotifiedMsgTs) && + Objects.equals(lastNotifiedAt, that.lastNotifiedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, threadId, lastNotifiedMsgTs, lastNotifiedAt); + } + + @NonNull + @Override + public String toString() { + return "DMLastNotified{" + + "id=" + id + + ", threadId='" + threadId + '\'' + + ", lastNotifiedMsgTs='" + lastNotifiedMsgTs + '\'' + + ", lastNotifiedAt='" + lastNotifiedAt + '\'' + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/db/repositories/DMLastNotifiedRepository.java b/app/src/main/java/awais/instagrabber/db/repositories/DMLastNotifiedRepository.java new file mode 100644 index 00000000..9cb15291 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/repositories/DMLastNotifiedRepository.java @@ -0,0 +1,126 @@ +package awais.instagrabber.db.repositories; + +import java.time.LocalDateTime; +import java.util.List; + +import awais.instagrabber.db.datasources.DMLastNotifiedDataSource; +import awais.instagrabber.db.entities.DMLastNotified; +import awais.instagrabber.utils.AppExecutors; + +public class DMLastNotifiedRepository { + private static final String TAG = DMLastNotifiedRepository.class.getSimpleName(); + + private static DMLastNotifiedRepository instance; + + private final AppExecutors appExecutors; + private final DMLastNotifiedDataSource dmLastNotifiedDataSource; + + private DMLastNotifiedRepository(final AppExecutors appExecutors, final DMLastNotifiedDataSource dmLastNotifiedDataSource) { + this.appExecutors = appExecutors; + this.dmLastNotifiedDataSource = dmLastNotifiedDataSource; + } + + public static DMLastNotifiedRepository getInstance(final DMLastNotifiedDataSource dmLastNotifiedDataSource) { + if (instance == null) { + instance = new DMLastNotifiedRepository(AppExecutors.getInstance(), dmLastNotifiedDataSource); + } + return instance; + } + + public void getDMLastNotified(final String threadId, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + final DMLastNotified dmLastNotified = dmLastNotifiedDataSource.getDMLastNotified(threadId); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (dmLastNotified == null) { + callback.onDataNotAvailable(); + return; + } + callback.onSuccess(dmLastNotified); + }); + }); + } + + public void getAllDMDmLastNotified(final RepositoryCallback> callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + final List allDMDmLastNotified = dmLastNotifiedDataSource.getAllDMDmLastNotified(); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (allDMDmLastNotified == null) { + callback.onDataNotAvailable(); + return; + } + // cachedAccounts = accounts; + callback.onSuccess(allDMDmLastNotified); + }); + }); + } + + public void insertOrUpdateDMLastNotified(final List dmLastNotifiedList, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + for (final DMLastNotified dmLastNotified : dmLastNotifiedList) { + dmLastNotifiedDataSource.insertOrUpdateDMLastNotified(dmLastNotified.getThreadId(), + dmLastNotified.getLastNotifiedMsgTs(), + dmLastNotified.getLastNotifiedAt()); + } + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } + + public void insertOrUpdateDMLastNotified(final String threadId, + final LocalDateTime lastNotifiedMsgTs, + final LocalDateTime lastNotifiedAt, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + dmLastNotifiedDataSource.insertOrUpdateDMLastNotified(threadId, lastNotifiedMsgTs, lastNotifiedAt); + final DMLastNotified updated = dmLastNotifiedDataSource.getDMLastNotified(threadId); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (updated == null) { + callback.onDataNotAvailable(); + return; + } + callback.onSuccess(updated); + }); + }); + } + + public void deleteDMLastNotified(final DMLastNotified dmLastNotified, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + dmLastNotifiedDataSource.deleteDMLastNotified(dmLastNotified); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } + + public void deleteAllDMLastNotified(final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + dmLastNotifiedDataSource.deleteAllDMLastNotified(); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } + +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/DMPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/DMPreferencesFragment.java new file mode 100644 index 00000000..5f84468a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/DMPreferencesFragment.java @@ -0,0 +1,201 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; +import android.content.Intent; +import android.text.Editable; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; + +import java.util.Objects; + +import awais.instagrabber.R; +import awais.instagrabber.customviews.helpers.TextWatcherAdapter; +import awais.instagrabber.databinding.PrefAutoRefreshDmFreqBinding; +import awais.instagrabber.services.DMSyncAlarmReceiver; +import awais.instagrabber.services.DMSyncService; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.Debouncer; +import awais.instagrabber.utils.TextUtils; + +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT; +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class DMPreferencesFragment extends BasePreferencesFragment { + private static final String TAG = DMPreferencesFragment.class.getSimpleName(); + + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + screen.addPreference(getMarkDMSeenPreference(context)); + screen.addPreference(getAutoRefreshDMPreference(context)); + screen.addPreference(getAutoRefreshDMFreqPreference(context)); + } + + private Preference getMarkDMSeenPreference(@NonNull final Context context) { + return PreferenceHelper.getSwitchPreference( + context, + Constants.DM_MARK_AS_SEEN, + R.string.dm_mark_as_seen_setting, + R.string.dm_mark_as_seen_setting_summary, + false, + null + ); + } + + private Preference getAutoRefreshDMPreference(@NonNull final Context context) { + return PreferenceHelper.getSwitchPreference( + context, + PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH, + R.string.enable_dm_auto_refesh, + -1, + false, + (preference, newValue) -> { + if (!(newValue instanceof Boolean)) return false; + final boolean enabled = (Boolean) newValue; + if (enabled) { + DMSyncAlarmReceiver.setAlarm(context); + return true; + } + DMSyncAlarmReceiver.cancelAlarm(context); + try { + final Context applicationContext = context.getApplicationContext(); + applicationContext.stopService(new Intent(applicationContext, DMSyncService.class)); + } catch (Exception e) { + Log.e(TAG, "getAutoRefreshDMPreference: ", e); + } + return true; + } + ); + } + + private Preference getAutoRefreshDMFreqPreference(@NonNull final Context context) { + return new AutoRefreshDMFrePreference(context); + } + + public static class AutoRefreshDMFrePreference extends Preference { + private static final String TAG = AutoRefreshDMFrePreference.class.getSimpleName(); + private static final String DEBOUNCE_KEY = "dm_sync_service_update"; + public static final int INTERVAL = 2000; + + private final Debouncer.Callback changeCallback; + + private Debouncer serviceUpdateDebouncer; + private PrefAutoRefreshDmFreqBinding binding; + + public AutoRefreshDMFrePreference(final Context context) { + super(context); + setLayoutResource(R.layout.pref_auto_refresh_dm_freq); + // setKey(key); + setIconSpaceReserved(false); + changeCallback = new Debouncer.Callback() { + @Override + public void call(final String key) { + DMSyncAlarmReceiver.setAlarm(context); + } + + @Override + public void onError(final Throwable t) { + Log.e(TAG, "onError: ", t); + } + }; + serviceUpdateDebouncer = new Debouncer<>(changeCallback, INTERVAL); + } + + @Override + public void onDependencyChanged(final Preference dependency, final boolean disableDependent) { + // super.onDependencyChanged(dependency, disableDependent); + if (binding == null) return; + binding.startText.setEnabled(!disableDependent); + binding.freqNum.setEnabled(!disableDependent); + binding.freqUnit.setEnabled(!disableDependent); + if (disableDependent) { + serviceUpdateDebouncer.terminate(); + return; + } + serviceUpdateDebouncer = new Debouncer<>(changeCallback, INTERVAL); + } + + @Override + public void onBindViewHolder(final PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + setDependency(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH); + binding = PrefAutoRefreshDmFreqBinding.bind(holder.itemView); + final Context context = getContext(); + if (context == null) return; + setupUnitSpinner(context); + setupNumberEditText(context); + } + + private void setupUnitSpinner(final Context context) { + final ArrayAdapter adapter = ArrayAdapter.createFromResource(context, + R.array.dm_auto_refresh_freq_unit_labels, + android.R.layout.simple_spinner_item); + final String[] values = context.getResources().getStringArray(R.array.dm_auto_refresh_freq_units); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + binding.freqUnit.setAdapter(adapter); + + String unit = settingsHelper.getString(PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT); + if (TextUtils.isEmpty(unit)) { + unit = "secs"; + } + int position = 0; + for (int i = 0; i < values.length; i++) { + if (Objects.equals(unit, values[i])) { + position = i; + break; + } + } + binding.freqUnit.setSelection(position); + binding.freqUnit.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(final AdapterView parent, final View view, final int position, final long id) { + settingsHelper.putString(PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT, values[position]); + if (!isEnabled()) { + serviceUpdateDebouncer.terminate(); + return; + } + serviceUpdateDebouncer.call(DEBOUNCE_KEY); + } + + @Override + public void onNothingSelected(final AdapterView parent) {} + }); + } + + private void setupNumberEditText(final Context context) { + int currentValue = settingsHelper.getInteger(PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER); + if (currentValue <= 0) { + currentValue = 5; + } + binding.freqNum.setText(String.valueOf(currentValue)); + binding.freqNum.addTextChangedListener(new TextWatcherAdapter() { + + @Override + public void afterTextChanged(final Editable s) { + if (TextUtils.isEmpty(s)) return; + try { + final int value = Integer.parseInt(s.toString()); + if (value <= 0) return; + settingsHelper.putInteger(PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER, value); + if (!isEnabled()) { + serviceUpdateDebouncer.terminate(); + return; + } + serviceUpdateDebouncer.call(DEBOUNCE_KEY); + } catch (Exception e) { + Log.e(TAG, "afterTextChanged: ", e); + } + } + }); + } + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java new file mode 100644 index 00000000..c2637b78 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java @@ -0,0 +1,101 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatButton; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; +import androidx.preference.SwitchPreferenceCompat; + +import com.google.android.material.switchmaterial.SwitchMaterial; + +import awais.instagrabber.R; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.DirectoryChooser; +import awais.instagrabber.utils.TextUtils; + +import static awais.instagrabber.utils.Constants.FOLDER_PATH; +import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO; +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class DownloadsPreferencesFragment extends BasePreferencesFragment { + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + screen.addPreference(getDownloadUserFolderPreference(context)); + screen.addPreference(getSaveToCustomFolderPreference(context)); + } + + private Preference getDownloadUserFolderPreference(@NonNull final Context context) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(Constants.DOWNLOAD_USER_FOLDER); + preference.setTitle(R.string.download_user_folder); + preference.setIconSpaceReserved(false); + return preference; + } + + private Preference getSaveToCustomFolderPreference(@NonNull final Context context) { + return new SaveToCustomFolderPreference(context, (resultCallback) -> new DirectoryChooser() + .setInitialDirectory(settingsHelper.getString(FOLDER_PATH)) + .setInteractionListener(file -> { + settingsHelper.putString(FOLDER_PATH, file.getAbsolutePath()); + resultCallback.onResult(file.getAbsolutePath()); + }) + .show(getParentFragmentManager(), null)); + } + + public static class SaveToCustomFolderPreference extends Preference { + private AppCompatTextView customPathTextView; + private final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener; + private final String key; + + public SaveToCustomFolderPreference(final Context context, final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener) { + super(context); + this.onSelectFolderButtonClickListener = onSelectFolderButtonClickListener; + key = Constants.FOLDER_SAVE_TO; + setLayoutResource(R.layout.pref_custom_folder); + setKey(key); + setTitle(R.string.save_to_folder); + setIconSpaceReserved(false); + } + + @Override + public void onBindViewHolder(final PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + final SwitchMaterial cbSaveTo = (SwitchMaterial) holder.findViewById(R.id.cbSaveTo); + final View buttonContainer = holder.findViewById(R.id.button_container); + customPathTextView = (AppCompatTextView) holder.findViewById(R.id.custom_path); + cbSaveTo.setOnCheckedChangeListener((buttonView, isChecked) -> { + settingsHelper.putBoolean(FOLDER_SAVE_TO, isChecked); + buttonContainer.setVisibility(isChecked ? View.VISIBLE : View.GONE); + final String customPath = settingsHelper.getString(FOLDER_PATH); + customPathTextView.setText(customPath); + }); + final boolean savedToEnabled = settingsHelper.getBoolean(key); + holder.itemView.setOnClickListener(v -> cbSaveTo.toggle()); + cbSaveTo.setChecked(savedToEnabled); + buttonContainer.setVisibility(savedToEnabled ? View.VISIBLE : View.GONE); + final AppCompatButton btnSaveTo = (AppCompatButton) holder.findViewById(R.id.btnSaveTo); + btnSaveTo.setOnClickListener(v -> { + if (onSelectFolderButtonClickListener == null) return; + onSelectFolderButtonClickListener.onClick(result -> { + if (TextUtils.isEmpty(result)) return; + customPathTextView.setText(result); + }); + }); + } + + public interface ResultCallback { + void onResult(String result); + } + + public interface OnSelectFolderButtonClickListener { + void onClick(ResultCallback resultCallback); + } + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/GeneralPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/GeneralPreferencesFragment.java new file mode 100644 index 00000000..deb7c277 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/GeneralPreferencesFragment.java @@ -0,0 +1,61 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; +import android.content.res.TypedArray; + +import androidx.annotation.NonNull; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreferenceCompat; + +import awais.instagrabber.R; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.TextUtils; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class GeneralPreferencesFragment extends BasePreferencesFragment { + + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + final String cookie = settingsHelper.getString(Constants.COOKIE); + final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; + if (isLoggedIn) { + screen.addPreference(getDefaultTabPreference(context)); + } + screen.addPreference(getUpdateCheckPreference(context)); + } + + private Preference getDefaultTabPreference(@NonNull final Context context) { + final ListPreference preference = new ListPreference(context); + preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); + final TypedArray mainNavIds = getResources().obtainTypedArray(R.array.main_nav_ids); + final int length = mainNavIds.length(); + final String[] values = new String[length]; + for (int i = 0; i < length; i++) { + final int resourceId = mainNavIds.getResourceId(i, -1); + if (resourceId < 0) continue; + values[i] = getResources().getResourceEntryName(resourceId); + } + mainNavIds.recycle(); + preference.setKey(Constants.DEFAULT_TAB); + preference.setTitle(R.string.pref_start_screen); + preference.setDialogTitle(R.string.pref_start_screen); + preference.setEntries(R.array.main_nav_ids_values); + preference.setEntryValues(values); + preference.setIconSpaceReserved(false); + return preference; + } + + private Preference getUpdateCheckPreference(@NonNull final Context context) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(Constants.CHECK_UPDATES); + preference.setTitle(R.string.update_check); + preference.setIconSpaceReserved(false); + return preference; + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/LocalePreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/LocalePreferencesFragment.java new file mode 100644 index 00000000..2c140157 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/LocalePreferencesFragment.java @@ -0,0 +1,48 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import awais.instagrabber.R; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.LocaleUtils; +import awais.instagrabber.utils.UserAgentUtils; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class LocalePreferencesFragment extends BasePreferencesFragment { + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + screen.addPreference(getLanguagePreference(context)); + } + + private Preference getLanguagePreference(@NonNull final Context context) { + final ListPreference preference = new ListPreference(context); + preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); + final int length = getResources().getStringArray(R.array.languages).length; + final String[] values = new String[length]; + for (int i = 0; i < length; i++) { + values[i] = String.valueOf(i); + } + preference.setKey(Constants.APP_LANGUAGE); + preference.setTitle(R.string.select_language); + preference.setDialogTitle(R.string.select_language); + preference.setEntries(R.array.languages); + preference.setIconSpaceReserved(false); + preference.setEntryValues(values); + preference.setOnPreferenceChangeListener((preference1, newValue) -> { + shouldRecreate(); + final int appUaCode = settingsHelper.getInteger(Constants.APP_UA_CODE); + final String appUa = UserAgentUtils.generateAppUA(appUaCode, LocaleUtils.getCurrentLocale().getLanguage()); + settingsHelper.putString(Constants.APP_UA, appUa); + return true; + }); + return preference; + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/NotificationsPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/NotificationsPreferencesFragment.java new file mode 100644 index 00000000..72f69f79 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/NotificationsPreferencesFragment.java @@ -0,0 +1,43 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import awais.instagrabber.R; +import awais.instagrabber.utils.Constants; + +public class NotificationsPreferencesFragment extends BasePreferencesFragment { + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + screen.addPreference(getActivityNotificationsPreference(context)); + screen.addPreference(getDMNotificationsPreference(context)); + } + + private Preference getActivityNotificationsPreference(@NonNull final Context context) { + return PreferenceHelper.getSwitchPreference( + context, + Constants.CHECK_ACTIVITY, + R.string.activity_setting, + -1, + false, + (preference, newValue) -> { + shouldRecreate(); + return true; + }); + } + + private Preference getDMNotificationsPreference(@NonNull final Context context) { + return PreferenceHelper.getSwitchPreference( + context, + PreferenceKeys.PREF_ENABLE_DM_NOTIFICATIONS, + R.string.enable_dm_notifications, + -1, + false, + null); + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java new file mode 100644 index 00000000..3e1885e6 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java @@ -0,0 +1,94 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreferenceCompat; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import awais.instagrabber.R; +import awais.instagrabber.dialogs.TimeSettingsDialog; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.Utils; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class PostPreferencesFragment extends BasePreferencesFragment { + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + // generalCategory.addPreference(getAutoPlayVideosPreference(context)); + screen.addPreference(getAlwaysMuteVideosPreference(context)); + screen.addPreference(getShowCaptionPreference(context)); + screen.addPreference(getPostTimeFormatPreference(context)); + } + + private Preference getAutoPlayVideosPreference(@NonNull final Context context) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(Constants.AUTOPLAY_VIDEOS); + preference.setTitle(R.string.post_viewer_autoplay_video); + preference.setIconSpaceReserved(false); + return preference; + } + + private Preference getAlwaysMuteVideosPreference(@NonNull final Context context) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(Constants.MUTED_VIDEOS); + preference.setTitle(R.string.post_viewer_muted_autoplay); + preference.setIconSpaceReserved(false); + return preference; + } + + private Preference getShowCaptionPreference(@NonNull final Context context) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(Constants.SHOW_CAPTIONS); + preference.setDefaultValue(true); + preference.setTitle(R.string.post_viewer_show_captions); + preference.setIconSpaceReserved(false); + return preference; + } + + private Preference getPostTimeFormatPreference(@NonNull final Context context) { + final Preference preference = new Preference(context); + preference.setTitle(R.string.time_settings); + preference.setSummary(Utils.datetimeParser.format(new Date())); + preference.setIconSpaceReserved(false); + preference.setOnPreferenceClickListener(preference1 -> { + new TimeSettingsDialog( + settingsHelper.getBoolean(Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED), + settingsHelper.getString(Constants.CUSTOM_DATE_TIME_FORMAT), + settingsHelper.getString(Constants.DATE_TIME_SELECTION), + settingsHelper.getBoolean(Constants.SWAP_DATE_TIME_FORMAT_ENABLED), + (isCustomFormat, + formatSelection, + spTimeFormatSelectedItemPosition, + spSeparatorSelectedItemPosition, + spDateFormatSelectedItemPosition, + selectedFormat, + currentFormat, + swapDateTime) -> { + if (isCustomFormat) { + settingsHelper.putString(Constants.CUSTOM_DATE_TIME_FORMAT, formatSelection); + } else { + final String formatSelectionUpdated = spTimeFormatSelectedItemPosition + ";" + + spSeparatorSelectedItemPosition + ';' + + spDateFormatSelectedItemPosition; // time;separator;date + settingsHelper.putString(Constants.DATE_TIME_FORMAT, selectedFormat); + settingsHelper.putString(Constants.DATE_TIME_SELECTION, formatSelectionUpdated); + } + settingsHelper.putBoolean(Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED, isCustomFormat); + settingsHelper.putBoolean(Constants.SWAP_DATE_TIME_FORMAT_ENABLED, swapDateTime); + Utils.datetimeParser = (SimpleDateFormat) currentFormat.clone(); + preference.setSummary(Utils.datetimeParser.format(new Date())); + } + ).show(getParentFragmentManager(), null); + return true; + }); + return preference; + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceHelper.java b/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceHelper.java new file mode 100644 index 00000000..824c18d7 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceHelper.java @@ -0,0 +1,30 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.SwitchPreferenceCompat; + +public final class PreferenceHelper { + + public static SwitchPreferenceCompat getSwitchPreference(@NonNull final Context context, + @NonNull final String key, + @StringRes final int titleResId, + @StringRes final int summaryResId, + final boolean iconSpaceReserved, + final OnPreferenceChangeListener onPreferenceChangeListener) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(key); + preference.setTitle(titleResId); + preference.setIconSpaceReserved(iconSpaceReserved); + if (summaryResId != -1) { + preference.setSummary(summaryResId); + } + if (onPreferenceChangeListener != null) { + preference.setOnPreferenceChangeListener(onPreferenceChangeListener); + } + return preference; + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.java b/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.java new file mode 100644 index 00000000..3f481685 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.java @@ -0,0 +1,8 @@ +package awais.instagrabber.fragments.settings; + +public final class PreferenceKeys { + public static final String PREF_ENABLE_DM_NOTIFICATIONS = "enable_dm_notifications"; + public static final String PREF_ENABLE_DM_AUTO_REFRESH = "enable_dm_auto_refresh"; + public static final String PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT = "enable_dm_auto_refresh_freq_unit"; + public static final String PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER = "enable_dm_auto_refresh_freq_number"; +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java index 00628d3f..b45739a0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java @@ -1,91 +1,55 @@ package awais.instagrabber.fragments.settings; import android.content.Context; -import android.content.res.TypedArray; -import android.view.View; import androidx.annotation.NonNull; -import androidx.appcompat.widget.AppCompatButton; -import androidx.appcompat.widget.AppCompatTextView; +import androidx.annotation.StringRes; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; -import androidx.preference.ListPreference; import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; -import androidx.preference.PreferenceViewHolder; -import androidx.preference.SwitchPreferenceCompat; -import com.google.android.material.switchmaterial.SwitchMaterial; +import com.google.common.collect.ImmutableList; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.util.List; import awais.instagrabber.R; -import awais.instagrabber.dialogs.TimeSettingsDialog; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.DirectoryChooser; -import awais.instagrabber.utils.LocaleUtils; import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.UserAgentUtils; -import awais.instagrabber.utils.Utils; -import static awais.instagrabber.utils.Constants.FOLDER_PATH; -import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToDm; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToDownloads; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToGeneral; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToLocale; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToNotifications; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToPost; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToStories; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToTheme; import static awais.instagrabber.utils.Utils.settingsHelper; public class SettingsPreferencesFragment extends BasePreferencesFragment { - private static final String TAG = "SettingsPrefsFrag"; - private boolean isLoggedIn; + private static final String TAG = SettingsPreferencesFragment.class.getSimpleName(); + private static final List screens = ImmutableList.of( + new SettingScreen(R.string.pref_category_general, actionSettingsToGeneral()), + new SettingScreen(R.string.pref_category_theme, actionSettingsToTheme()), + new SettingScreen(R.string.pref_category_locale, actionSettingsToLocale()), + new SettingScreen(R.string.pref_category_post, actionSettingsToPost()), + new SettingScreen(R.string.pref_category_stories, actionSettingsToStories(), true), + new SettingScreen(R.string.pref_category_dm, actionSettingsToDm(), true), + new SettingScreen(R.string.pref_category_notifications, actionSettingsToNotifications(), true), + new SettingScreen(R.string.pref_category_downloads, actionSettingsToDownloads()) + ); @Override void setupPreferenceScreen(final PreferenceScreen screen) { - final String cookie = settingsHelper.getString(Constants.COOKIE); - isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; final Context context = getContext(); if (context == null) return; - final PreferenceCategory generalCategory = new PreferenceCategory(context); - screen.addPreference(generalCategory); - generalCategory.setTitle(R.string.pref_category_general); - generalCategory.setIconSpaceReserved(false); - generalCategory.addPreference(getThemePreference(context)); - generalCategory.addPreference(getDefaultTabPreference()); - generalCategory.addPreference(getUpdateCheckPreference()); - // generalCategory.addPreference(getAutoPlayVideosPreference()); - generalCategory.addPreference(getAlwaysMuteVideosPreference()); - generalCategory.addPreference(getShowCaptionPreference()); - - // screen.addPreference(getDivider(context)); - // final PreferenceCategory themeCategory = new PreferenceCategory(context); - // screen.addPreference(themeCategory); - // themeCategory.setTitle(R.string.pref_category_theme); - // themeCategory.setIconSpaceReserved(false); - // themeCategory.addPreference(getAmoledThemePreference()); - - final PreferenceCategory downloadsCategory = new PreferenceCategory(context); - screen.addPreference(downloadsCategory); - downloadsCategory.setTitle(R.string.pref_category_downloads); - downloadsCategory.setIconSpaceReserved(false); - downloadsCategory.addPreference(getDownloadUserFolderPreference()); - downloadsCategory.addPreference(getSaveToCustomFolderPreference()); - - final PreferenceCategory localeCategory = new PreferenceCategory(context); - screen.addPreference(localeCategory); - localeCategory.setTitle(R.string.pref_category_locale); - localeCategory.setIconSpaceReserved(false); - localeCategory.addPreference(getLanguagePreference()); - localeCategory.addPreference(getPostTimePreference()); - - if (isLoggedIn) { - final PreferenceCategory loggedInUsersPreferenceCategory = new PreferenceCategory(context); - screen.addPreference(loggedInUsersPreferenceCategory); - loggedInUsersPreferenceCategory.setIconSpaceReserved(false); - loggedInUsersPreferenceCategory.setTitle(R.string.login_settings); - loggedInUsersPreferenceCategory.addPreference(getStorySortPreference()); - loggedInUsersPreferenceCategory.addPreference(getMarkStoriesSeenPreference()); - loggedInUsersPreferenceCategory.addPreference(getMarkDMSeenPreference()); - loggedInUsersPreferenceCategory.addPreference(getEnableActivityNotificationsPreference()); + final String cookie = settingsHelper.getString(Constants.COOKIE); + final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; + for (final SettingScreen settingScreen : screens) { + if (settingScreen.isLoginRequired() && !isLoggedIn) continue; + screen.addPreference(getNavPreference(context, settingScreen)); } // else { // final PreferenceCategory anonUsersPreferenceCategory = new PreferenceCategory(context); @@ -95,275 +59,43 @@ public class SettingsPreferencesFragment extends BasePreferencesFragment { // } } - private Preference getLanguagePreference() { - final Context context = getContext(); - if (context == null) return null; - final ListPreference preference = new ListPreference(context); - preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); - final int length = getResources().getStringArray(R.array.languages).length; - final String[] values = new String[length]; - for (int i = 0; i < length; i++) { - values[i] = String.valueOf(i); - } - preference.setKey(Constants.APP_LANGUAGE); - preference.setTitle(R.string.select_language); - preference.setDialogTitle(R.string.select_language); - preference.setEntries(R.array.languages); - preference.setIconSpaceReserved(false); - preference.setEntryValues(values); - preference.setOnPreferenceChangeListener((preference1, newValue) -> { - shouldRecreate(); - final int appUaCode = settingsHelper.getInteger(Constants.APP_UA_CODE); - final String appUa = UserAgentUtils.generateAppUA(appUaCode, LocaleUtils.getCurrentLocale().getLanguage()); - settingsHelper.putString(Constants.APP_UA, appUa); - return true; - }); - return preference; - } - - private Preference getDefaultTabPreference() { - final Context context = getContext(); - if (context == null) return null; - final ListPreference preference = new ListPreference(context); - preference.setEnabled(isLoggedIn); - preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); - final TypedArray mainNavIds = getResources().obtainTypedArray(R.array.main_nav_ids); - final int length = mainNavIds.length(); - final String[] values = new String[length]; - for (int i = 0; i < length; i++) { - final int resourceId = mainNavIds.getResourceId(i, -1); - if (resourceId < 0) continue; - values[i] = getResources().getResourceEntryName(resourceId); - } - mainNavIds.recycle(); - preference.setKey(Constants.DEFAULT_TAB); - preference.setTitle(R.string.pref_start_screen); - preference.setDialogTitle(R.string.pref_start_screen); - preference.setEntries(R.array.main_nav_ids_values); - preference.setEntryValues(values); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getUpdateCheckPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.CHECK_UPDATES); - preference.setTitle(R.string.update_check); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getThemePreference(@NonNull final Context context) { + private Preference getNavPreference(@NonNull final Context context, + @NonNull final SettingScreen settingScreen) { final Preference preference = new Preference(context); - preference.setTitle(R.string.pref_category_theme); - // preference.setIcon(R.drawable.ic_format_paint_24); + preference.setTitle(settingScreen.getTitleResId()); preference.setIconSpaceReserved(false); preference.setOnPreferenceClickListener(preference1 -> { - final NavDirections navDirections = SettingsPreferencesFragmentDirections.actionSettingsPreferencesFragmentToThemePreferencesFragment(); - NavHostFragment.findNavController(this).navigate(navDirections); + NavHostFragment.findNavController(this).navigate(settingScreen.getDirections()); return true; }); return preference; } - private Preference getDownloadUserFolderPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.DOWNLOAD_USER_FOLDER); - preference.setTitle(R.string.download_user_folder); - preference.setIconSpaceReserved(false); - return preference; - } + private static class SettingScreen { + private final int titleResId; + private final NavDirections directions; + private final boolean loginRequired; - private Preference getSaveToCustomFolderPreference() { - final Context context = getContext(); - if (context == null) return null; - return new SaveToCustomFolderPreference(context, (resultCallback) -> new DirectoryChooser() - .setInitialDirectory(settingsHelper.getString(FOLDER_PATH)) - .setInteractionListener(file -> { - settingsHelper.putString(FOLDER_PATH, file.getAbsolutePath()); - resultCallback.onResult(file.getAbsolutePath()); - }) - .show(getParentFragmentManager(), null)); - } - - private Preference getAutoPlayVideosPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.AUTOPLAY_VIDEOS); - preference.setTitle(R.string.post_viewer_autoplay_video); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getAlwaysMuteVideosPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.MUTED_VIDEOS); - preference.setTitle(R.string.post_viewer_muted_autoplay); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getShowCaptionPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.SHOW_CAPTIONS); - preference.setDefaultValue(true); - preference.setTitle(R.string.post_viewer_show_captions); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getStorySortPreference() { - final Context context = getContext(); - if (context == null) return null; - final ListPreference preference = new ListPreference(context); - preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); - final int length = getResources().getStringArray(R.array.story_sorts).length; - final String[] values = new String[length]; - for (int i = 0; i < length; i++) { - values[i] = String.valueOf(i); - } - preference.setKey(Constants.STORY_SORT); - preference.setTitle(R.string.story_sort_setting); - preference.setDialogTitle(R.string.story_sort_setting); - preference.setEntries(R.array.story_sorts); - preference.setIconSpaceReserved(false); - preference.setEntryValues(values); - return preference; - } - - private Preference getMarkStoriesSeenPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.MARK_AS_SEEN); - preference.setTitle(R.string.mark_as_seen_setting); - preference.setSummary(R.string.mark_as_seen_setting_summary); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getMarkDMSeenPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.DM_MARK_AS_SEEN); - preference.setTitle(R.string.dm_mark_as_seen_setting); - preference.setSummary(R.string.dm_mark_as_seen_setting_summary); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getEnableActivityNotificationsPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.CHECK_ACTIVITY); - preference.setTitle(R.string.activity_setting); - preference.setIconSpaceReserved(false); - preference.setOnPreferenceChangeListener((preference1, newValue) -> { - shouldRecreate(); - return true; - }); - return preference; - } - - private Preference getPostTimePreference() { - final Context context = getContext(); - if (context == null) return null; - final Preference preference = new Preference(context); - preference.setTitle(R.string.time_settings); - preference.setSummary(Utils.datetimeParser.format(new Date())); - preference.setIconSpaceReserved(false); - preference.setOnPreferenceClickListener(preference1 -> { - new TimeSettingsDialog( - settingsHelper.getBoolean(Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED), - settingsHelper.getString(Constants.CUSTOM_DATE_TIME_FORMAT), - settingsHelper.getString(Constants.DATE_TIME_SELECTION), - settingsHelper.getBoolean(Constants.SWAP_DATE_TIME_FORMAT_ENABLED), - (isCustomFormat, - formatSelection, - spTimeFormatSelectedItemPosition, - spSeparatorSelectedItemPosition, - spDateFormatSelectedItemPosition, - selectedFormat, - currentFormat, - swapDateTime) -> { - if (isCustomFormat) { - settingsHelper.putString(Constants.CUSTOM_DATE_TIME_FORMAT, formatSelection); - } else { - final String formatSelectionUpdated = spTimeFormatSelectedItemPosition + ";" - + spSeparatorSelectedItemPosition + ';' - + spDateFormatSelectedItemPosition; // time;separator;date - settingsHelper.putString(Constants.DATE_TIME_FORMAT, selectedFormat); - settingsHelper.putString(Constants.DATE_TIME_SELECTION, formatSelectionUpdated); - } - settingsHelper.putBoolean(Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED, isCustomFormat); - settingsHelper.putBoolean(Constants.SWAP_DATE_TIME_FORMAT_ENABLED, swapDateTime); - Utils.datetimeParser = (SimpleDateFormat) currentFormat.clone(); - preference.setSummary(Utils.datetimeParser.format(new Date())); - } - ).show(getParentFragmentManager(), null); - return true; - }); - return preference; - } - - public static class SaveToCustomFolderPreference extends Preference { - private AppCompatTextView customPathTextView; - private final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener; - private final String key; - - public SaveToCustomFolderPreference(final Context context, final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener) { - super(context); - this.onSelectFolderButtonClickListener = onSelectFolderButtonClickListener; - key = Constants.FOLDER_SAVE_TO; - setLayoutResource(R.layout.pref_custom_folder); - setKey(key); - setTitle(R.string.save_to_folder); - setIconSpaceReserved(false); + public SettingScreen(@StringRes final int titleResId, final NavDirections directions) { + this(titleResId, directions, false); } - @Override - public void onBindViewHolder(final PreferenceViewHolder holder) { - super.onBindViewHolder(holder); - final SwitchMaterial cbSaveTo = (SwitchMaterial) holder.findViewById(R.id.cbSaveTo); - final View buttonContainer = holder.findViewById(R.id.button_container); - customPathTextView = (AppCompatTextView) holder.findViewById(R.id.custom_path); - cbSaveTo.setOnCheckedChangeListener((buttonView, isChecked) -> { - settingsHelper.putBoolean(FOLDER_SAVE_TO, isChecked); - buttonContainer.setVisibility(isChecked ? View.VISIBLE : View.GONE); - final String customPath = settingsHelper.getString(FOLDER_PATH); - customPathTextView.setText(customPath); - }); - final boolean savedToEnabled = settingsHelper.getBoolean(key); - holder.itemView.setOnClickListener(v -> cbSaveTo.toggle()); - cbSaveTo.setChecked(savedToEnabled); - buttonContainer.setVisibility(savedToEnabled ? View.VISIBLE : View.GONE); - final AppCompatButton btnSaveTo = (AppCompatButton) holder.findViewById(R.id.btnSaveTo); - btnSaveTo.setOnClickListener(v -> { - if (onSelectFolderButtonClickListener == null) return; - onSelectFolderButtonClickListener.onClick(result -> { - if (TextUtils.isEmpty(result)) return; - customPathTextView.setText(result); - }); - }); + public SettingScreen(@StringRes final int titleResId, final NavDirections directions, final boolean loginRequired) { + this.titleResId = titleResId; + this.directions = directions; + this.loginRequired = loginRequired; } - public interface ResultCallback { - void onResult(String result); + public int getTitleResId() { + return titleResId; } - public interface OnSelectFolderButtonClickListener { - void onClick(ResultCallback resultCallback); + public NavDirections getDirections() { + return directions; + } + + public boolean isLoginRequired() { + return loginRequired; } } } diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/StoriesPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/StoriesPreferencesFragment.java new file mode 100644 index 00000000..8337f7c3 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/StoriesPreferencesFragment.java @@ -0,0 +1,48 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreferenceCompat; + +import awais.instagrabber.R; +import awais.instagrabber.utils.Constants; + +public class StoriesPreferencesFragment extends BasePreferencesFragment { + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + screen.addPreference(getStorySortPreference(context)); + screen.addPreference(getMarkStoriesSeenPreference(context)); + } + + private Preference getStorySortPreference(@NonNull final Context context) { + final ListPreference preference = new ListPreference(context); + preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); + final int length = getResources().getStringArray(R.array.story_sorts).length; + final String[] values = new String[length]; + for (int i = 0; i < length; i++) { + values[i] = String.valueOf(i); + } + preference.setKey(Constants.STORY_SORT); + preference.setTitle(R.string.story_sort_setting); + preference.setDialogTitle(R.string.story_sort_setting); + preference.setEntries(R.array.story_sorts); + preference.setIconSpaceReserved(false); + preference.setEntryValues(values); + return preference; + } + + private Preference getMarkStoriesSeenPreference(@NonNull final Context context) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(Constants.MARK_AS_SEEN); + preference.setTitle(R.string.mark_as_seen_setting); + preference.setSummary(R.string.mark_as_seen_setting_summary); + preference.setIconSpaceReserved(false); + return preference; + } +} diff --git a/app/src/main/java/awais/instagrabber/managers/InboxManager.java b/app/src/main/java/awais/instagrabber/managers/InboxManager.java index 81ddd195..a41f0fc9 100644 --- a/app/src/main/java/awais/instagrabber/managers/InboxManager.java +++ b/app/src/main/java/awais/instagrabber/managers/InboxManager.java @@ -323,6 +323,7 @@ public final class InboxManager { if (insertIndex < 0) return; synchronized (this.inbox) { final DirectInbox currentDirectInbox = getCurrentDirectInbox(); + if (currentDirectInbox == null) return; final List threadsCopy = new LinkedList<>(currentDirectInbox.getThreads()); threadsCopy.add(insertIndex, thread); try { @@ -338,6 +339,7 @@ public final class InboxManager { public void removeThread(@NonNull final String threadId) { synchronized (this.inbox) { final DirectInbox currentDirectInbox = getCurrentDirectInbox(); + if (currentDirectInbox == null) return; final List threadsCopy = currentDirectInbox.getThreads() .stream() .filter(t -> !t.getThreadId().equals(threadId)) diff --git a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java index bf23a8bc..7e00e165 100644 --- a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java @@ -382,6 +382,8 @@ public final class ThreadManager { } public void fetchPendingRequests() { + final Boolean isGroup = this.isGroup.getValue(); + if (isGroup == null || !isGroup) return; final Call request = service.participantRequests(threadId, 1, null); request.enqueue(new Callback() { diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java index 0bda29c7..35f8994d 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java @@ -2,6 +2,9 @@ package awais.instagrabber.repositories.responses.directmessages; import androidx.annotation.NonNull; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Date; import java.util.List; import java.util.Objects; @@ -41,6 +44,7 @@ public class DirectItem implements Cloneable { private Date date; private boolean isPending; private boolean showForwardAttribution; + private LocalDateTime localDateTime; public DirectItem(final String itemId, final long userId, @@ -214,6 +218,13 @@ public class DirectItem implements Cloneable { return date; } + public LocalDateTime getLocalDateTime() { + if (localDateTime == null) { + localDateTime = Instant.ofEpochMilli(timestamp / 1000).atZone(ZoneId.systemDefault()).toLocalDateTime();; + } + return localDateTime; + } + public void setItemId(final String itemId) { this.itemId = itemId; } diff --git a/app/src/main/java/awais/instagrabber/services/ActivityCheckerService.java b/app/src/main/java/awais/instagrabber/services/ActivityCheckerService.java index f0ccea70..2ae44b79 100644 --- a/app/src/main/java/awais/instagrabber/services/ActivityCheckerService.java +++ b/app/src/main/java/awais/instagrabber/services/ActivityCheckerService.java @@ -133,6 +133,6 @@ public class ActivityCheckerService extends Service { final Intent intent = new Intent(getApplicationContext(), MainActivity.class) .setAction(Constants.ACTION_SHOW_ACTIVITY) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); - return PendingIntent.getActivity(getApplicationContext(), 1738, intent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getActivity(getApplicationContext(), Constants.SHOW_ACTIVITY_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT); } } diff --git a/app/src/main/java/awais/instagrabber/services/DMSyncAlarmReceiver.java b/app/src/main/java/awais/instagrabber/services/DMSyncAlarmReceiver.java new file mode 100644 index 00000000..4fc37973 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/services/DMSyncAlarmReceiver.java @@ -0,0 +1,87 @@ +package awais.instagrabber.services; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; + +import awais.instagrabber.fragments.settings.PreferenceKeys; +import awais.instagrabber.utils.Constants; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class DMSyncAlarmReceiver extends BroadcastReceiver { + private static final String TAG = DMSyncAlarmReceiver.class.getSimpleName(); + + @Override + public void onReceive(final Context context, final Intent intent) { + final boolean enabled = settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH); + if (!enabled) { + // If somehow the alarm was triggered even when auto refresh is disabled + cancelAlarm(context); + return; + } + try { + final Context applicationContext = context.getApplicationContext(); + ContextCompat.startForegroundService(applicationContext, new Intent(applicationContext, DMSyncService.class)); + } catch (Exception e) { + Log.e(TAG, "onReceive: ", e); + } + } + + public static void setAlarm(@NonNull final Context context) { + Log.d(TAG, "setting DMSyncService Alarm"); + final AlarmManager alarmManager = getAlarmManager(context); + if (alarmManager == null) return; + final PendingIntent pendingIntent = getPendingIntent(context); + alarmManager.setInexactRepeating(AlarmManager.RTC, System.currentTimeMillis(), getIntervalMillis(), pendingIntent); + } + + public static void cancelAlarm(@NonNull final Context context) { + Log.d(TAG, "cancelling DMSyncService Alarm"); + final AlarmManager alarmManager = getAlarmManager(context); + if (alarmManager == null) return; + final PendingIntent pendingIntent = getPendingIntent(context); + alarmManager.cancel(pendingIntent); + } + + private static AlarmManager getAlarmManager(@NonNull final Context context) { + return (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE); + } + + private static PendingIntent getPendingIntent(@NonNull final Context context) { + final Context applicationContext = context.getApplicationContext(); + final Intent intent = new Intent(applicationContext, DMSyncAlarmReceiver.class); + return PendingIntent.getBroadcast(applicationContext, + Constants.DM_SYNC_SERVICE_REQUEST_CODE, + intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + private static long getIntervalMillis() { + int amount = settingsHelper.getInteger(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER); + if (amount <= 0) { + amount = 5; + } + final String unit = settingsHelper.getString(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT); + final TemporalUnit temporalUnit; + switch (unit) { + case "mins": + temporalUnit = ChronoUnit.MINUTES; + break; + default: + case "secs": + temporalUnit = ChronoUnit.SECONDS; + } + return Duration.of(amount, temporalUnit).toMillis(); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/services/DMSyncService.java b/app/src/main/java/awais/instagrabber/services/DMSyncService.java new file mode 100644 index 00000000..a4c18aab --- /dev/null +++ b/app/src/main/java/awais/instagrabber/services/DMSyncService.java @@ -0,0 +1,248 @@ +package awais.instagrabber.services; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.IBinder; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.lifecycle.LifecycleService; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import awais.instagrabber.R; +import awais.instagrabber.activities.MainActivity; +import awais.instagrabber.db.datasources.DMLastNotifiedDataSource; +import awais.instagrabber.db.entities.DMLastNotified; +import awais.instagrabber.db.repositories.DMLastNotifiedRepository; +import awais.instagrabber.db.repositories.RepositoryCallback; +import awais.instagrabber.fragments.settings.PreferenceKeys; +import awais.instagrabber.managers.DirectMessagesManager; +import awais.instagrabber.managers.InboxManager; +import awais.instagrabber.models.Resource; +import awais.instagrabber.repositories.responses.directmessages.DirectInbox; +import awais.instagrabber.repositories.responses.directmessages.DirectItem; +import awais.instagrabber.repositories.responses.directmessages.DirectThread; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadLastSeenAt; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.DMUtils; +import awais.instagrabber.utils.DateUtils; +import awais.instagrabber.utils.Utils; + +public class DMSyncService extends LifecycleService { + private static final String TAG = DMSyncService.class.getSimpleName(); + + private InboxManager inboxManager; + private DMLastNotifiedRepository dmLastNotifiedRepository; + private Map dmLastNotifiedMap; + + @Override + public void onCreate() { + super.onCreate(); + startForeground(Constants.DM_CHECK_NOTIFICATION_ID, buildForegroundNotification()); + Log.d(TAG, "onCreate: Service created"); + final DirectMessagesManager directMessagesManager = DirectMessagesManager.getInstance(); + inboxManager = directMessagesManager.getInboxManager(); + dmLastNotifiedRepository = DMLastNotifiedRepository.getInstance(DMLastNotifiedDataSource.getInstance(getApplicationContext())); + } + + private void parseUnread(@NonNull final DirectInbox directInbox) { + dmLastNotifiedRepository.getAllDMDmLastNotified(new RepositoryCallback>() { + @Override + public void onSuccess(final List result) { + dmLastNotifiedMap = result != null + ? result.stream().collect(Collectors.toMap(DMLastNotified::getThreadId, Function.identity())) + : Collections.emptyMap(); + parseUnreadActual(directInbox); + } + + @Override + public void onDataNotAvailable() { + dmLastNotifiedMap = Collections.emptyMap(); + parseUnreadActual(directInbox); + } + }); + // Log.d(TAG, "inbox observer: " + directInbox); + } + + private void parseUnreadActual(@NonNull final DirectInbox directInbox) { + final List threads = directInbox.getThreads(); + final ImmutableMap.Builder> unreadMessagesMapBuilder = ImmutableMap.builder(); + if (threads == null) { + stopSelf(); + return; + } + for (final DirectThread thread : threads) { + if (thread.isMuted()) continue; + final boolean read = DMUtils.isRead(thread); + if (read) continue; + final List unreadMessages = getUnreadMessages(thread); + if (unreadMessages.isEmpty()) continue; + unreadMessagesMapBuilder.put(thread.getThreadId(), unreadMessages); + } + final Map> unreadMessagesMap = unreadMessagesMapBuilder.build(); + if (unreadMessagesMap.isEmpty()) { + stopSelf(); + return; + } + showNotification(directInbox, unreadMessagesMap); + final LocalDateTime now = LocalDateTime.now(); + // Update db + final ImmutableList.Builder lastNotifiedListBuilder = ImmutableList.builder(); + for (final Map.Entry> unreadMessagesEntry : unreadMessagesMap.entrySet()) { + final List unreadItems = unreadMessagesEntry.getValue(); + final DirectItem latestItem = unreadItems.get(unreadItems.size() - 1); + lastNotifiedListBuilder.add(new DMLastNotified(0, + unreadMessagesEntry.getKey(), + latestItem.getLocalDateTime(), + now)); + } + dmLastNotifiedRepository.insertOrUpdateDMLastNotified( + lastNotifiedListBuilder.build(), + new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + stopSelf(); + } + + @Override + public void onDataNotAvailable() { + stopSelf(); + } + } + ); + } + + @NonNull + private List getUnreadMessages(@NonNull final DirectThread thread) { + final List items = thread.getItems(); + if (items == null) return Collections.emptyList(); + final DMLastNotified dmLastNotified = dmLastNotifiedMap.get(thread.getThreadId()); + final long viewerId = thread.getViewerId(); + final Map lastSeenAt = thread.getLastSeenAt(); + final ImmutableList.Builder unreadListBuilder = ImmutableList.builder(); + int count = 0; + for (final DirectItem item : items) { + if (item == null) continue; + if (item.getUserId() == viewerId) break; // Reached a message from the viewer, it is assumed the viewer has read the next messages + final boolean read = DMUtils.isRead(item, lastSeenAt, Collections.singletonList(viewerId)); + if (read) break; + if (dmLastNotified != null && dmLastNotified.getLastNotifiedMsgTs() != null) { + if (count == 0 && DateUtils.isBeforeOrEqual(item.getLocalDateTime(), dmLastNotified.getLastNotifiedMsgTs())) { + // The first unread item has been notified and hence all subsequent items can be ignored + // since the items are in desc timestamp order + break; + } + } + unreadListBuilder.add(item); + count++; + // Inbox style notification only allows 6 lines + if (count >= 6) break; + } + // Reversing, so that oldest messages are on top + return unreadListBuilder.build().reverse(); + } + + private void showNotification(final DirectInbox directInbox, + final Map> unreadMessagesMap) { + final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) return; + for (final Map.Entry> unreadMessagesEntry : unreadMessagesMap.entrySet()) { + final Optional directThreadOptional = getThread(directInbox, unreadMessagesEntry.getKey()); + if (!directThreadOptional.isPresent()) continue; + final DirectThread thread = directThreadOptional.get(); + final DirectItem firstDirectItem = thread.getFirstDirectItem(); + if (firstDirectItem == null) continue; + final List unreadMessages = unreadMessagesEntry.getValue(); + final NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); + inboxStyle.setBigContentTitle(thread.getThreadTitle()); + for (final DirectItem item : unreadMessages) { + inboxStyle.addLine(DMUtils.getMessageString(thread, getResources(), thread.getViewerId(), item)); + } + final Notification notification = new NotificationCompat.Builder(this, Constants.DM_UNREAD_CHANNEL_ID) + .setStyle(inboxStyle) + .setSmallIcon(R.drawable.ic_round_mode_comment_24) + .setContentTitle(thread.getThreadTitle()) + .setContentText(DMUtils.getMessageString(thread, getResources(), thread.getViewerId(), firstDirectItem)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setGroup(Constants.GROUP_KEY_DM) + .setAutoCancel(true) + .setContentIntent(getThreadPendingIntent(thread.getThreadId(), thread.getThreadTitle())) + .build(); + notificationManager.notify(Constants.DM_UNREAD_PARENT_NOTIFICATION_ID, notification); + } + } + + private Optional getThread(@NonNull final DirectInbox directInbox, final String threadId) { + return directInbox.getThreads() + .stream() + .filter(thread -> Objects.equals(thread.getThreadId(), threadId)) + .findFirst(); + } + + @NonNull + private PendingIntent getThreadPendingIntent(final String threadId, final String threadTitle) { + final Intent intent = new Intent(getApplicationContext(), MainActivity.class) + .setAction(Constants.ACTION_SHOW_DM_THREAD) + .putExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_ID, threadId) + .putExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_TITLE, threadTitle) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); + return PendingIntent.getActivity(getApplicationContext(), Constants.SHOW_DM_THREAD, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + @Override + public int onStartCommand(final Intent intent, final int flags, final int startId) { + super.onStartCommand(intent, flags, startId); + final boolean notificationsEnabled = Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_NOTIFICATIONS); + inboxManager.getInbox().observe(this, inboxResource -> { + if (!notificationsEnabled || inboxResource == null || inboxResource.status != Resource.Status.SUCCESS) { + stopSelf(); + return; + } + final DirectInbox directInbox = inboxResource.data; + if (directInbox == null) { + stopSelf(); + return; + } + parseUnread(directInbox); + }); + Log.d(TAG, "onStartCommand: refreshing inbox"); + inboxManager.refresh(); + return START_NOT_STICKY; + } + + @Override + public IBinder onBind(@NonNull final Intent intent) { + super.onBind(intent); + return null; + } + + private Notification buildForegroundNotification() { + final Resources resources = getResources(); + return new NotificationCompat.Builder(this, Constants.SILENT_NOTIFICATIONS_CHANNEL_ID) + .setOngoing(true) + .setSound(null) + .setContentTitle(resources.getString(R.string.app_name)) + .setContentText(resources.getString(R.string.checking_for_new_messages)) + .setSmallIcon(R.mipmap.ic_launcher) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setGroup(Constants.GROUP_KEY_SILENT_NOTIFICATIONS) + .build(); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.java b/app/src/main/java/awais/instagrabber/utils/Constants.java index 1b06283c..62e5bf5a 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.java +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -58,6 +58,8 @@ public final class Constants { // Notification ids public static final int ACTIVITY_NOTIFICATION_ID = 10; + public static final int DM_UNREAD_PARENT_NOTIFICATION_ID = 20; + public static final int DM_CHECK_NOTIFICATION_ID = 11; // see https://github.com/dilame/instagram-private-api/blob/master/src/core/constants.ts public static final String SUPPORTED_CAPABILITIES = "[ { \"name\": \"SUPPORTED_SDK_VERSIONS\", \"value\":" + @@ -74,12 +76,6 @@ public final class Constants { public static final String FDROID_SHA1_FINGERPRINT = "C1661EB8FD09F618307E687786D5E5056F65084D"; public static final String SKIPPED_VERSION = "skipped_version"; public static final String DEFAULT_TAB = "default_tab"; - public static final String ACTIVITY_CHANNEL_ID = "activity"; - public static final String DOWNLOAD_CHANNEL_ID = "download"; - public static final String ACTIVITY_CHANNEL_NAME = "Activity"; - public static final String DOWNLOAD_CHANNEL_NAME = "Downloads"; - public static final String NOTIF_GROUP_NAME = "awais.instagrabber.InstaNotif"; - public static final String ACTION_SHOW_ACTIVITY = "show_activity"; public static final String PREF_DARK_THEME = "dark_theme"; public static final String PREF_LIGHT_THEME = "light_theme"; public static final String DEFAULT_HASH_TAG_PIC = "https://www.instagram.com/static/images/hashtag/search-hashtag-default-avatar.png/1d8417c9a4f5.png"; @@ -94,4 +90,27 @@ public final class Constants { public static final String PREF_SAVED_POSTS_LAYOUT = "saved_posts_layout"; public static final String PREF_EMOJI_VARIANTS = "emoji_variants"; public static final String PREF_REACTIONS = "reactions"; + + public static final String ACTIVITY_CHANNEL_ID = "activity"; + public static final String ACTIVITY_CHANNEL_NAME = "Activity"; + public static final String DOWNLOAD_CHANNEL_ID = "download"; + public static final String DOWNLOAD_CHANNEL_NAME = "Downloads"; + public static final String DM_UNREAD_CHANNEL_ID = "dmUnread"; + public static final String DM_UNREAD_CHANNEL_NAME = "Messages"; + public static final String SILENT_NOTIFICATIONS_CHANNEL_ID = "silentNotifications"; + public static final String SILENT_NOTIFICATIONS_CHANNEL_NAME = "Silent notifications"; + + public static final String NOTIF_GROUP_NAME = "awais.instagrabber.InstaNotif"; + public static final String GROUP_KEY_DM = "awais.instagrabber.MESSAGES"; + public static final String GROUP_KEY_SILENT_NOTIFICATIONS = "awais.instagrabber.SILENT_NOTIFICATIONS"; + + public static final int SHOW_ACTIVITY_REQUEST_CODE = 1738; + public static final int SHOW_DM_THREAD = 2000; + public static final int DM_SYNC_SERVICE_REQUEST_CODE = 3000; + + public static final String ACTION_SHOW_ACTIVITY = "show_activity"; + public static final String ACTION_SHOW_DM_THREAD = "show_dm_thread"; + + public static final String DM_THREAD_ACTION_EXTRA_THREAD_ID = "thread_id"; + public static final String DM_THREAD_ACTION_EXTRA_THREAD_TITLE = "thread_title"; } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/DMUtils.java b/app/src/main/java/awais/instagrabber/utils/DMUtils.java new file mode 100644 index 00000000..6ff6126a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/DMUtils.java @@ -0,0 +1,273 @@ +package awais.instagrabber.utils; + +import android.content.res.Resources; + +import androidx.annotation.NonNull; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import awais.instagrabber.R; +import awais.instagrabber.models.enums.DirectItemType; +import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.repositories.responses.directmessages.DirectItem; +import awais.instagrabber.repositories.responses.directmessages.DirectItemReelShare; +import awais.instagrabber.repositories.responses.directmessages.DirectItemVisualMedia; +import awais.instagrabber.repositories.responses.directmessages.DirectThread; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadLastSeenAt; +import awais.instagrabber.repositories.responses.directmessages.RavenExpiringMediaActionSummary; + +public final class DMUtils { + public static boolean isRead(final DirectItem item, + @NonNull final Map lastSeenAt, + final List userIdsToCheck) { + // Further check if directStory exists + // if (read && directStory != null) { + // read = false; + // } + return lastSeenAt.entrySet() + .stream() + .filter(entry -> userIdsToCheck.contains(entry.getKey())) + .anyMatch(entry -> { + final String userLastSeenTsString = entry.getValue().getTimestamp(); + if (userLastSeenTsString == null) return false; + final long userTs = Long.parseLong(userLastSeenTsString); + final long itemTs = item.getTimestamp(); + return userTs >= itemTs; + }); + } + + public static boolean isRead(@NonNull final DirectThread thread) { + final boolean read; + if (thread.getDirectStory() != null) { + return false; + } + final DirectItem item = thread.getFirstDirectItem(); + final long viewerId = thread.getViewerId(); + if (item != null && item.getUserId() == viewerId) { + // if last item was sent by user, then it is read (even though we have auto read unchecked?) + read = true; + } else { + final Map lastSeenAtMap = thread.getLastSeenAt(); + read = isRead(item, lastSeenAtMap, Collections.singletonList(viewerId)); + } + return read; + } + + public static String getMessageString(@NonNull final DirectThread thread, + final Resources resources, + final long viewerId, + final DirectItem item) { + final long senderId = item.getUserId(); + final DirectItemType itemType = item.getItemType(); + String subtitle = null; + final String username = getUsername(thread.getUsers(), senderId, viewerId, resources); + String message = ""; + if (itemType == null) { + message = resources.getString(R.string.dms_inbox_raven_message_unknown); + } else { + switch (itemType) { + case TEXT: + message = item.getText(); + break; + case LIKE: + message = item.getLike(); + break; + case LINK: + message = item.getLink().getText(); + break; + case PLACEHOLDER: + message = item.getPlaceholder().getMessage(); + break; + case MEDIA_SHARE: + subtitle = resources.getString(R.string.dms_inbox_shared_post, username != null ? username : "", + item.getMediaShare().getUser().getUsername()); + break; + case ANIMATED_MEDIA: + subtitle = resources.getString(R.string.dms_inbox_shared_gif, username != null ? username : ""); + break; + case PROFILE: + subtitle = resources + .getString(R.string.dms_inbox_shared_profile, username != null ? username : "", item.getProfile().getUsername()); + break; + case LOCATION: + subtitle = resources + .getString(R.string.dms_inbox_shared_location, username != null ? username : "", item.getLocation().getName()); + break; + case MEDIA: { + final MediaItemType mediaType = item.getMedia().getMediaType(); + subtitle = getMediaSpecificSubtitle(username, resources, mediaType); + break; + } + case STORY_SHARE: { + final String reelType = item.getStoryShare().getReelType(); + if (reelType == null) { + subtitle = item.getStoryShare().getTitle(); + } else { + final int format = reelType.equals("highlight_reel") + ? R.string.dms_inbox_shared_highlight + : R.string.dms_inbox_shared_story; + subtitle = resources.getString(format, username != null ? username : "", + item.getStoryShare().getMedia().getUser().getUsername()); + } + break; + } + case VOICE_MEDIA: + subtitle = resources.getString(R.string.dms_inbox_shared_voice, username != null ? username : ""); + break; + case ACTION_LOG: + subtitle = item.getActionLog().getDescription(); + break; + case VIDEO_CALL_EVENT: + subtitle = item.getVideoCallEvent().getDescription(); + break; + case CLIP: + subtitle = resources.getString(R.string.dms_inbox_shared_clip, username != null ? username : "", + item.getClip().getClip().getUser().getUsername()); + break; + case FELIX_SHARE: + subtitle = resources.getString(R.string.dms_inbox_shared_igtv, username != null ? username : "", + item.getFelixShare().getVideo().getUser().getUsername()); + break; + case RAVEN_MEDIA: + subtitle = getRavenMediaSubtitle(item, resources, username); + break; + case REEL_SHARE: + final DirectItemReelShare reelShare = item.getReelShare(); + if (reelShare == null) { + subtitle = ""; + break; + } + final String reelType = reelShare.getType(); + switch (reelType) { + case "reply": + if (viewerId == item.getUserId()) { + subtitle = resources.getString(R.string.dms_inbox_replied_story_outgoing, reelShare.getText()); + } else { + subtitle = resources + .getString(R.string.dms_inbox_replied_story_incoming, username != null ? username : "", reelShare.getText()); + } + break; + case "mention": + if (viewerId == item.getUserId()) { + // You mentioned the other person + final long mentionedUserId = item.getReelShare().getMentionedUserId(); + final String otherUsername = getUsername(thread.getUsers(), mentionedUserId, viewerId, resources); + subtitle = resources.getString(R.string.dms_inbox_mentioned_story_outgoing, otherUsername); + } else { + // They mentioned you + subtitle = resources.getString(R.string.dms_inbox_mentioned_story_incoming, username != null ? username : ""); + } + break; + case "reaction": + if (viewerId == item.getUserId()) { + subtitle = resources.getString(R.string.dms_inbox_reacted_story_outgoing, reelShare.getText()); + } else { + subtitle = resources + .getString(R.string.dms_inbox_reacted_story_incoming, username != null ? username : "", reelShare.getText()); + } + break; + default: + subtitle = ""; + break; + } + break; + default: + message = resources.getString(R.string.dms_inbox_raven_message_unknown); + } + } + if (subtitle == null) { + if (thread.isGroup() || (!thread.isGroup() && senderId == viewerId)) { + subtitle = String.format("%s: %s", username != null ? username : "", message); + } else { + subtitle = message; + } + } + return subtitle; + } + + public static String getUsername(final List users, + final long userId, + final long viewerId, + final Resources resources) { + if (userId == viewerId) { + return resources.getString(R.string.you); + } + final Optional senderOptional = users.stream() + .filter(Objects::nonNull) + .filter(user -> user.getPk() == userId) + .findFirst(); + return senderOptional.map(User::getUsername).orElse(null); + } + + public static String getMediaSpecificSubtitle(final String username, final Resources resources, final MediaItemType mediaType) { + final String userSharedAnImage = resources.getString(R.string.dms_inbox_shared_image, username != null ? username : ""); + final String userSharedAVideo = resources.getString(R.string.dms_inbox_shared_video, username != null ? username : ""); + final String userSentAMessage = resources.getString(R.string.dms_inbox_shared_message, username != null ? username : ""); + String subtitle; + switch (mediaType) { + case MEDIA_TYPE_IMAGE: + subtitle = userSharedAnImage; + break; + case MEDIA_TYPE_VIDEO: + subtitle = userSharedAVideo; + break; + default: + subtitle = userSentAMessage; + break; + } + return subtitle; + } + + private static String getRavenMediaSubtitle(final DirectItem item, + final Resources resources, + final String username) { + String subtitle = "↗ "; + final DirectItemVisualMedia visualMedia = item.getVisualMedia(); + final RavenExpiringMediaActionSummary summary = visualMedia.getExpiringMediaActionSummary(); + if (summary != null) { + final RavenExpiringMediaActionSummary.ActionType expiringMediaType = summary.getType(); + int textRes = 0; + switch (expiringMediaType) { + case DELIVERED: + textRes = R.string.dms_inbox_raven_media_delivered; + break; + case SENT: + textRes = R.string.dms_inbox_raven_media_sent; + break; + case OPENED: + textRes = R.string.dms_inbox_raven_media_opened; + break; + case REPLAYED: + textRes = R.string.dms_inbox_raven_media_replayed; + break; + case SENDING: + textRes = R.string.dms_inbox_raven_media_sending; + break; + case BLOCKED: + textRes = R.string.dms_inbox_raven_media_blocked; + break; + case SUGGESTED: + textRes = R.string.dms_inbox_raven_media_suggested; + break; + case SCREENSHOT: + textRes = R.string.dms_inbox_raven_media_screenshot; + break; + case CANNOT_DELIVER: + textRes = R.string.dms_inbox_raven_media_cant_deliver; + break; + } + if (textRes > 0) { + subtitle += resources.getString(textRes); + } + return subtitle; + } + final MediaItemType mediaType = visualMedia.getMedia().getMediaType(); + subtitle = getMediaSpecificSubtitle(username, resources, mediaType); + return subtitle; + } +} diff --git a/app/src/main/java/awais/instagrabber/utils/DateUtils.java b/app/src/main/java/awais/instagrabber/utils/DateUtils.java index 648fbc59..6f8fdcd1 100644 --- a/app/src/main/java/awais/instagrabber/utils/DateUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/DateUtils.java @@ -2,6 +2,7 @@ package awais.instagrabber.utils; import androidx.annotation.NonNull; +import java.time.LocalDateTime; import java.util.Calendar; import java.util.Date; import java.util.Locale; @@ -34,4 +35,8 @@ public final class DateUtils { final Calendar calendar = Calendar.getInstance(Locale.getDefault()); return -(calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000); } + + public static boolean isBeforeOrEqual(final LocalDateTime localDateTime, final LocalDateTime comparedTo) { + return localDateTime.isBefore(comparedTo) || localDateTime.isEqual(comparedTo); + } } diff --git a/app/src/main/java/awais/instagrabber/utils/FlavorTown.java b/app/src/main/java/awais/instagrabber/utils/FlavorTown.java index 3200b499..ee859700 100755 --- a/app/src/main/java/awais/instagrabber/utils/FlavorTown.java +++ b/app/src/main/java/awais/instagrabber/utils/FlavorTown.java @@ -105,11 +105,11 @@ public final class FlavorTown { if (settingsHelper.getInteger(Constants.PREV_INSTALL_VERSION) < BuildConfig.VERSION_CODE) { int appUaCode = settingsHelper.getInteger(Constants.APP_UA_CODE); int browserUaCode = settingsHelper.getInteger(Constants.BROWSER_UA_CODE); - if (browserUaCode == -1) { + if (browserUaCode == -1 || browserUaCode >= UserAgentUtils.browsers.length) { browserUaCode = ThreadLocalRandom.current().nextInt(0, UserAgentUtils.browsers.length); settingsHelper.putInteger(Constants.BROWSER_UA_CODE, browserUaCode); } - if (appUaCode == -1) { + if (appUaCode == -1 || appUaCode >= UserAgentUtils.devices.length) { appUaCode = ThreadLocalRandom.current().nextInt(0, UserAgentUtils.devices.length); settingsHelper.putInteger(Constants.APP_UA_CODE, appUaCode); } diff --git a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java index d9ef6d66..17aa3684 100644 --- a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java @@ -13,7 +13,6 @@ import org.json.JSONObject; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import awais.instagrabber.BuildConfig; import awais.instagrabber.models.StoryModel; @@ -31,8 +30,6 @@ import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.MediaCandidate; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.VideoVersion; -import awais.instagrabber.repositories.responses.directmessages.DirectItem; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadLastSeenAt; import awaisomereport.LogCollector; public final class ResponseBodyUtils { @@ -1123,25 +1120,6 @@ public final class ResponseBodyUtils { return candidate.getUrl(); } - public static boolean isRead(final DirectItem item, - final Map lastSeenAt, - final List userIdsToCheck) { - // Further check if directStory exists - // if (read && directStory != null) { - // read = false; - // } - return lastSeenAt.entrySet() - .stream() - .filter(entry -> userIdsToCheck.contains(entry.getKey())) - .anyMatch(entry -> { - final String userLastSeenTsString = entry.getValue().getTimestamp(); - if (userLastSeenTsString == null) return false; - final long userTs = Long.parseLong(userLastSeenTsString); - final long itemTs = item.getTimestamp(); - return userTs >= itemTs; - }); - } - public static StoryModel parseBroadcastItem(final JSONObject data) throws JSONException { final StoryModel model = new StoryModel(data.getString("id"), data.getString("cover_frame_url"), diff --git a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java index dd96f41e..e3426f99 100755 --- a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -8,6 +8,10 @@ import androidx.annotation.NonNull; import androidx.annotation.StringDef; import androidx.appcompat.app.AppCompatDelegate; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_NOTIFICATIONS; import static awais.instagrabber.utils.Constants.APP_LANGUAGE; import static awais.instagrabber.utils.Constants.APP_THEME; import static awais.instagrabber.utils.Constants.APP_UA; @@ -130,14 +134,14 @@ public final class SettingsHelper { CUSTOM_DATE_TIME_FORMAT, DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT, PREF_PROFILE_POSTS_LAYOUT, PREF_TOPIC_POSTS_LAYOUT, PREF_HASHTAG_POSTS_LAYOUT, PREF_LOCATION_POSTS_LAYOUT, PREF_LIKED_POSTS_LAYOUT, PREF_TAGGED_POSTS_LAYOUT, PREF_SAVED_POSTS_LAYOUT, - STORY_SORT, PREF_EMOJI_VARIANTS, PREF_REACTIONS}) + STORY_SORT, PREF_EMOJI_VARIANTS, PREF_REACTIONS, PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT}) public @interface StringSettings {} @StringDef({DOWNLOAD_USER_FOLDER, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS, SHOW_CAPTIONS, CUSTOM_DATE_TIME_FORMAT_ENABLED, MARK_AS_SEEN, DM_MARK_AS_SEEN, CHECK_ACTIVITY, - CHECK_UPDATES, SWAP_DATE_TIME_FORMAT_ENABLED}) + CHECK_UPDATES, SWAP_DATE_TIME_FORMAT_ENABLED, PREF_ENABLE_DM_NOTIFICATIONS, PREF_ENABLE_DM_AUTO_REFRESH}) public @interface BooleanSettings {} - @StringDef({PREV_INSTALL_VERSION, BROWSER_UA_CODE, APP_UA_CODE}) + @StringDef({PREV_INSTALL_VERSION, BROWSER_UA_CODE, APP_UA_CODE, PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER}) public @interface IntegerSettings {} } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_round_mode_comment_24.xml b/app/src/main/res/drawable/ic_round_mode_comment_24.xml new file mode 100644 index 00000000..366bca72 --- /dev/null +++ b/app/src/main/res/drawable/ic_round_mode_comment_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/pref_auto_refresh_dm_freq.xml b/app/src/main/res/layout/pref_auto_refresh_dm_freq.xml new file mode 100644 index 00000000..45930499 --- /dev/null +++ b/app/src/main/res/layout/pref_auto_refresh_dm_freq.xml @@ -0,0 +1,33 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/more_nav_graph.xml b/app/src/main/res/navigation/more_nav_graph.xml index dd199b6f..6d35b9b7 100644 --- a/app/src/main/res/navigation/more_nav_graph.xml +++ b/app/src/main/res/navigation/more_nav_graph.xml @@ -78,8 +78,29 @@ android:name="awais.instagrabber.fragments.settings.SettingsPreferencesFragment" android:label="@string/action_settings"> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 49bcf692..e9d9d6d6 100755 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -124,4 +124,12 @@ @style/AppTheme.Dark.Black @style/AppTheme.Dark.MaterialDark + + secs + mins + + + @string/secs + @string/mins + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 664e8045..64e61a51 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -474,4 +474,14 @@ Accept You No pending requests + Checking for new messages + Stories + DM + Notifications + Post + Enable DM notifications + Auto refresh messages + Auto refresh every + secs + mins