DM sync service

This commit is contained in:
Ammar Githam 2021-03-14 00:21:31 +09:00
parent 605f15ee3a
commit df5a96e035
39 changed files with 2076 additions and 600 deletions

View File

@ -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"

View File

@ -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')"
]
}
}

View File

@ -13,7 +13,6 @@
android:name="android.hardware.camera.any"
android:required="false" />
<application
android:name=".InstaGrabberApplication"
android:allowBackup="true"
@ -37,8 +36,8 @@
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<!--<action android:name="android.intent.action.SEARCH" />-->
<!--<action android:name="android.intent.action.WEB_SEARCH" />-->
<!-- <action android:name="android.intent.action.SEARCH" /> -->
<!-- <action android:name="android.intent.action.WEB_SEARCH" /> -->
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@ -147,6 +146,14 @@
<service
android:name=".services.DeleteImageIntentService"
android:exported="false" />
<service
android:name=".services.DMSyncService"
android:exported="false" />
<receiver
android:name=".services.DMSyncAlarmReceiver"
android:enabled="true"
android:exported="false" />
<uses-library
android:name="org.apache.http.legacy"
@ -156,4 +163,5 @@
android:name="fontProviderRequests"
android:value="Noto Color Emoji Compat" />
</application>
</manifest>

View File

@ -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<NavController>() {
@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);

View File

@ -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<User> users,
final long userId,
final long viewerId,
final Resources resources) {
if (userId == viewerId) {
return resources.getString(R.string.you);
}
final Optional<User> 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<Long, DirectThreadLastSeenAt> 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);

View File

@ -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);

View File

@ -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<Favorite> backupOldFavorites(@NonNull final SupportSQLiteDatabase db) {
// check if old favorites table had the column query_display

View File

@ -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();
}
}

View File

@ -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<DMLastNotified> getAllDMDmLastNotified();
@Query("SELECT * FROM dm_last_notified WHERE thread_id = :threadId")
DMLastNotified findDMLastNotifiedByThreadId(String threadId);
@Insert(onConflict = OnConflictStrategy.REPLACE)
List<Long> insertDMLastNotified(DMLastNotified... dmLastNotified);
@Update
void updateDMLastNotified(DMLastNotified... dmLastNotified);
@Delete
void deleteDMLastNotified(DMLastNotified... dmLastNotified);
@Query("DELETE from dm_last_notified")
void deleteAllDMLastNotified();
}

View File

@ -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<DMLastNotified> 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();
}
}

View File

@ -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 + '\'' +
'}';
}
}

View File

@ -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<DMLastNotified> 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<List<DMLastNotified>> callback) {
// request on the I/O thread
appExecutors.diskIO().execute(() -> {
final List<DMLastNotified> 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<DMLastNotified> dmLastNotifiedList,
final RepositoryCallback<Void> 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<DMLastNotified> 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<Void> 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<Void> 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);
});
});
}
}

View File

@ -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<String> changeCallback;
private Debouncer<String> 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<String>() {
@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<CharSequence> 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);
}
}
});
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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";
}

View File

@ -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<SettingScreen> 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;
}
}
}

View File

@ -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;
}
}

View File

@ -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<DirectThread> 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<DirectThread> threadsCopy = currentDirectInbox.getThreads()
.stream()
.filter(t -> !t.getThreadId().equals(threadId))

View File

@ -382,6 +382,8 @@ public final class ThreadManager {
}
public void fetchPendingRequests() {
final Boolean isGroup = this.isGroup.getValue();
if (isGroup == null || !isGroup) return;
final Call<DirectThreadParticipantRequestsResponse> request = service.participantRequests(threadId, 1, null);
request.enqueue(new Callback<DirectThreadParticipantRequestsResponse>() {

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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<String, DMLastNotified> 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<List<DMLastNotified>>() {
@Override
public void onSuccess(final List<DMLastNotified> 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<DirectThread> threads = directInbox.getThreads();
final ImmutableMap.Builder<String, List<DirectItem>> 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<DirectItem> unreadMessages = getUnreadMessages(thread);
if (unreadMessages.isEmpty()) continue;
unreadMessagesMapBuilder.put(thread.getThreadId(), unreadMessages);
}
final Map<String, List<DirectItem>> unreadMessagesMap = unreadMessagesMapBuilder.build();
if (unreadMessagesMap.isEmpty()) {
stopSelf();
return;
}
showNotification(directInbox, unreadMessagesMap);
final LocalDateTime now = LocalDateTime.now();
// Update db
final ImmutableList.Builder<DMLastNotified> lastNotifiedListBuilder = ImmutableList.builder();
for (final Map.Entry<String, List<DirectItem>> unreadMessagesEntry : unreadMessagesMap.entrySet()) {
final List<DirectItem> 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<Void>() {
@Override
public void onSuccess(final Void result) {
stopSelf();
}
@Override
public void onDataNotAvailable() {
stopSelf();
}
}
);
}
@NonNull
private List<DirectItem> getUnreadMessages(@NonNull final DirectThread thread) {
final List<DirectItem> items = thread.getItems();
if (items == null) return Collections.emptyList();
final DMLastNotified dmLastNotified = dmLastNotifiedMap.get(thread.getThreadId());
final long viewerId = thread.getViewerId();
final Map<Long, DirectThreadLastSeenAt> lastSeenAt = thread.getLastSeenAt();
final ImmutableList.Builder<DirectItem> 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<String, List<DirectItem>> unreadMessagesMap) {
final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager == null) return;
for (final Map.Entry<String, List<DirectItem>> unreadMessagesEntry : unreadMessagesMap.entrySet()) {
final Optional<DirectThread> 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<DirectItem> 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<DirectThread> 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();
}
}

View File

@ -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";
}

View File

@ -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<Long, DirectThreadLastSeenAt> lastSeenAt,
final List<Long> 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<Long, DirectThreadLastSeenAt> 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<User> users,
final long userId,
final long viewerId,
final Resources resources) {
if (userId == viewerId) {
return resources.getString(R.string.you);
}
final Optional<User> 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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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<Long, DirectThreadLastSeenAt> lastSeenAt,
final List<Long> 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"),

View File

@ -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 {}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M22,4c0,-1.1 -0.9,-2 -2,-2H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h14l4,4V4z"/>
</vector>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:clickable="false"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeight"
android:orientation="horizontal"
android:paddingStart="15dp"
android:paddingEnd="15dp">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/start_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/auto_refresh_every"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/freq_num"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:inputType="number"
android:textAlignment="center" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/freq_unit"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -78,8 +78,29 @@
android:name="awais.instagrabber.fragments.settings.SettingsPreferencesFragment"
android:label="@string/action_settings">
<action
android:id="@+id/action_settingsPreferencesFragment_to_themePreferencesFragment"
android:id="@+id/action_settings_to_theme"
app:destination="@id/themePreferencesFragment" />
<action
android:id="@+id/action_settings_to_locale"
app:destination="@id/localePreferencesFragment" />
<action
android:id="@+id/action_settings_to_general"
app:destination="@id/generalPreferencesFragment" />
<action
android:id="@+id/action_settings_to_downloads"
app:destination="@id/downloadsPreferencesFragment" />
<action
android:id="@+id/action_settings_to_dm"
app:destination="@id/DMPreferencesFragment" />
<action
android:id="@+id/action_settings_to_stories"
app:destination="@id/storiesPreferencesFragment" />
<action
android:id="@+id/action_settings_to_notifications"
app:destination="@id/notificationsPreferencesFragment" />
<action
android:id="@+id/action_settings_to_post"
app:destination="@id/postPreferencesFragment" />
</fragment>
<fragment
android:id="@+id/aboutFragment"
@ -97,4 +118,32 @@
android:id="@+id/backupPreferencesFragment"
android:name="awais.instagrabber.fragments.settings.BackupPreferencesFragment"
android:label="@string/backup_and_restore" />
<fragment
android:id="@+id/localePreferencesFragment"
android:name="awais.instagrabber.fragments.settings.LocalePreferencesFragment"
android:label="@string/pref_category_locale" />
<fragment
android:id="@+id/generalPreferencesFragment"
android:name="awais.instagrabber.fragments.settings.GeneralPreferencesFragment"
android:label="@string/pref_category_general" />
<fragment
android:id="@+id/downloadsPreferencesFragment"
android:name="awais.instagrabber.fragments.settings.DownloadsPreferencesFragment"
android:label="@string/pref_category_downloads" />
<fragment
android:id="@+id/DMPreferencesFragment"
android:name="awais.instagrabber.fragments.settings.DMPreferencesFragment"
android:label="@string/pref_category_dm" />
<fragment
android:id="@+id/storiesPreferencesFragment"
android:name="awais.instagrabber.fragments.settings.StoriesPreferencesFragment"
android:label="@string/pref_category_stories" />
<fragment
android:id="@+id/notificationsPreferencesFragment"
android:name="awais.instagrabber.fragments.settings.NotificationsPreferencesFragment"
android:label="@string/pref_category_notifications" />
<fragment
android:id="@+id/postPreferencesFragment"
android:name="awais.instagrabber.fragments.settings.PostPreferencesFragment"
android:label="PostPreferencesFragment" />
</navigation>

View File

@ -124,4 +124,12 @@
<item>@style/AppTheme.Dark.Black</item>
<item>@style/AppTheme.Dark.MaterialDark</item>
</string-array>
<string-array name="dm_auto_refresh_freq_units">
<item>secs</item>
<item>mins</item>
</string-array>
<string-array name="dm_auto_refresh_freq_unit_labels">
<item>@string/secs</item>
<item>@string/mins</item>
</string-array>
</resources>

View File

@ -474,4 +474,14 @@
<string name="accept">Accept</string>
<string name="you">You</string>
<string name="no_pending_requests">No pending requests</string>
<string name="checking_for_new_messages">Checking for new messages</string>
<string name="pref_category_stories">Stories</string>
<string name="pref_category_dm">DM</string>
<string name="pref_category_notifications">Notifications</string>
<string name="pref_category_post">Post</string>
<string name="enable_dm_notifications">Enable DM notifications</string>
<string name="enable_dm_auto_refesh">Auto refresh messages</string>
<string name="auto_refresh_every">Auto refresh every</string>
<string name="secs">secs</string>
<string name="mins">mins</string>
</resources>