Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added logging all checks to send a reply for debugging purpose #395

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
129 changes: 129 additions & 0 deletions app/schemas/com.parishod.watomatic.model.logs.MessageLogsDB/3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "e8b6351e87f979cedde00cb90e2b55e5",
"entities": [
{
"tableName": "message_logs",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `notif_id` TEXT, `notif_title` TEXT, `notif_arrived_time` INTEGER NOT NULL, `notif_is_replied` INTEGER NOT NULL, `notif_replied_msg` TEXT, `notif_reply_time` INTEGER NOT NULL, `notif_event` TEXT, FOREIGN KEY(`index`) REFERENCES `app_packages`(`index`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notifId",
"columnName": "notif_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "notifTitle",
"columnName": "notif_title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "notifArrivedTime",
"columnName": "notif_arrived_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notifIsReplied",
"columnName": "notif_is_replied",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notifRepliedMsg",
"columnName": "notif_replied_msg",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "notifReplyTime",
"columnName": "notif_reply_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notifEvent",
"columnName": "notif_event",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_message_logs_index",
"unique": false,
"columnNames": [
"index"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_message_logs_index` ON `${TABLE_NAME}` (`index`)"
}
],
"foreignKeys": [
{
"table": "app_packages",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"index"
],
"referencedColumns": [
"index"
]
}
]
},
{
"tableName": "app_packages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `package_name` TEXT)",
"fields": [
{
"fieldPath": "index",
"columnName": "index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"index"
],
"autoGenerate": true
},
"indices": [],
"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, 'e8b6351e87f979cedde00cb90e2b55e5')"
]
}
}
156 changes: 7 additions & 149 deletions app/src/main/java/com/parishod/watomatic/NotificationService.java
Original file line number Diff line number Diff line change
@@ -1,71 +1,22 @@
package com.parishod.watomatic;

import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.text.SpannableString;
import android.util.Log;
import com.parishod.watomatic.model.interfaces.OnMessageReplied;
import com.parishod.watomatic.model.utils.MessagingHelper;

import androidx.core.app.RemoteInput;

import com.parishod.watomatic.model.CustomRepliesData;
import com.parishod.watomatic.model.preferences.PreferencesManager;
import com.parishod.watomatic.model.utils.ContactsHelper;
import com.parishod.watomatic.model.utils.DbUtils;
import com.parishod.watomatic.model.utils.NotificationHelper;
import com.parishod.watomatic.model.utils.NotificationUtils;

import static java.lang.Math.max;

public class NotificationService extends NotificationListenerService {
private final String TAG = NotificationService.class.getSimpleName();
CustomRepliesData customRepliesData;
private DbUtils dbUtils;
public class NotificationService extends NotificationListenerService implements OnMessageReplied {

@Override
public void onNotificationPosted(StatusBarNotification sbn) {
super.onNotificationPosted(sbn);
if (canReply(sbn) && shouldReply(sbn)) {
sendReply(sbn);
}
new MessagingHelper(this, this).handleMessage(sbn);
}

private boolean canReply(StatusBarNotification sbn) {
return isServiceEnabled() &&
isSupportedPackage(sbn) &&
NotificationUtils.isNewNotification(sbn) &&
isGroupMessageAndReplyAllowed(sbn) &&
canSendReplyNow(sbn);
}

private boolean shouldReply(StatusBarNotification sbn) {
PreferencesManager prefs = PreferencesManager.getPreferencesInstance(this);
boolean isGroup = sbn.getNotification().extras.getBoolean("android.isGroupConversation");

//Check contact based replies
if (prefs.isContactReplyEnabled() && !isGroup) {
//Title contains sender name (at least on WhatsApp)
String senderName = sbn.getNotification().extras.getString("android.title");
//Check if should reply to contact
boolean isNameSelected =
(ContactsHelper.getInstance(this).hasContactPermission()
&& prefs.getReplyToNames().contains(senderName)) ||
prefs.getCustomReplyNames().contains(senderName);
if ((isNameSelected && prefs.isContactReplyBlacklistMode()) ||
!isNameSelected && !prefs.isContactReplyBlacklistMode()) {
//If contact is on the list and contact reply is on blacklist mode,
// or contact is not in the list and reply is on whitelist mode,
// we don't want to reply
return false;
}
}

//Check more conditions on future feature implementations

//If we got here, all conditions to reply are met
return true;
@Override
public void onMessageReplied(String key) {
cancelNotification(key);
}

@Override
Expand All @@ -74,97 +25,4 @@ public int onStartCommand(Intent intent, int flags, int startId) {
//START_STICKY to order the system to restart your service as soon as possible when it was killed.
return START_STICKY;
}

private void sendReply(StatusBarNotification sbn) {
NotificationWear notificationWear = NotificationUtils.extractWearNotification(sbn);
// Possibly transient or non-user notification from WhatsApp like
// "Checking for new messages" or "WhatsApp web is Active"
if (notificationWear.getRemoteInputs().isEmpty()) {
return;
}

customRepliesData = CustomRepliesData.getInstance(this);

RemoteInput[] remoteInputs = new RemoteInput[notificationWear.getRemoteInputs().size()];

Intent localIntent = new Intent();
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Bundle localBundle = new Bundle();//notificationWear.bundle;
int i = 0;
for (RemoteInput remoteIn : notificationWear.getRemoteInputs()) {
remoteInputs[i] = remoteIn;
// This works. Might need additional parameter to make it for Hangouts? (notification_tag?)
localBundle.putCharSequence(remoteInputs[i].getResultKey(), customRepliesData.getTextToSendOrElse());
i++;
}

RemoteInput.addResultsToIntent(remoteInputs, localIntent, localBundle);
try {
if (notificationWear.getPendingIntent() != null) {
if (dbUtils == null) {
dbUtils = new DbUtils(getApplicationContext());
}
dbUtils.logReply(sbn, NotificationUtils.getTitle(sbn));
notificationWear.getPendingIntent().send(this, 0, localIntent);
if (PreferencesManager.getPreferencesInstance(this).isShowNotificationEnabled()) {
NotificationHelper.getInstance(getApplicationContext()).sendNotification(sbn.getNotification().extras.getString("android.title"), sbn.getNotification().extras.getString("android.text"), sbn.getPackageName());
}
cancelNotification(sbn.getKey());
if (canPurgeMessages()) {
dbUtils.purgeMessageLogs();
PreferencesManager.getPreferencesInstance(this).setPurgeMessageTime(System.currentTimeMillis());
}
}
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "replyToLastNotification error: " + e.getLocalizedMessage());
}
}

private boolean canPurgeMessages() {
//Added L to avoid numeric overflow expression
//https://stackoverflow.com/questions/43801874/numeric-overflow-in-expression-manipulating-timestamps
long daysBeforePurgeInMS = 30 * 24 * 60 * 60 * 1000L;
return (System.currentTimeMillis() - PreferencesManager.getPreferencesInstance(this).getLastPurgedTime()) > daysBeforePurgeInMS;
}

private boolean isSupportedPackage(StatusBarNotification sbn) {
return PreferencesManager.getPreferencesInstance(this)
.getEnabledApps()
.contains(sbn.getPackageName());
}

private boolean canSendReplyNow(StatusBarNotification sbn) {
// Do not reply to consecutive notifications from same person/group that arrive in below time
// This helps to prevent infinite loops when users on both end uses Watomatic or similar app
int DELAY_BETWEEN_REPLY_IN_MILLISEC = 10 * 1000;

String title = NotificationUtils.getTitle(sbn);
String selfDisplayName = sbn.getNotification().extras.getString("android.selfDisplayName");
if (title != null && title.equalsIgnoreCase(selfDisplayName)) { //to protect double reply in case where if notification is not dismissed and existing notification is updated with our reply
return false;
}
if (dbUtils == null) {
dbUtils = new DbUtils(getApplicationContext());
}
long timeDelay = PreferencesManager.getPreferencesInstance(this).getAutoReplyDelay();
return (System.currentTimeMillis() - dbUtils.getLastRepliedTime(sbn.getPackageName(), title) >= max(timeDelay, DELAY_BETWEEN_REPLY_IN_MILLISEC));
}

private boolean isGroupMessageAndReplyAllowed(StatusBarNotification sbn) {
String rawTitle = NotificationUtils.getTitleRaw(sbn);
//android.text returning SpannableString
SpannableString rawText = SpannableString.valueOf("" + sbn.getNotification().extras.get("android.text"));
// Detect possible group image message by checking for colon and text starts with camera icon #181
boolean isPossiblyAnImageGrpMsg = ((rawTitle != null) && (": ".contains(rawTitle) || "@ ".contains(rawTitle)))
&& ((rawText != null) && rawText.toString().contains("\uD83D\uDCF7"));
if (!sbn.getNotification().extras.getBoolean("android.isGroupConversation")) {
return !isPossiblyAnImageGrpMsg;
} else {
return PreferencesManager.getPreferencesInstance(this).isGroupReplyEnabled();
}
}

private boolean isServiceEnabled() {
return PreferencesManager.getPreferencesInstance(this).isServiceEnabled();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import com.parishod.watomatic.R;
import com.parishod.watomatic.model.utils.AutoStartHelper;
import com.parishod.watomatic.model.utils.CustomDialog;
import com.parishod.watomatic.model.utils.ServieUtils;

public class SettingsFragment extends PreferenceFragmentCompat {
Expand Down Expand Up @@ -40,6 +41,16 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
return true;
});
}

Preference sendAppLogsPref = findPreference(getString(R.string.pref_send_app_logs));
if (sendAppLogsPref != null) {
sendAppLogsPref.setOnPreferenceClickListener(preference -> {
if(getActivity() != null)
new CustomDialog(getActivity()).showAppLogsShareDialog();
return true;
});
}

}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.parishod.watomatic.model.interfaces;

public interface OnMessageReplied {
void onMessageReplied(String key);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ public interface AppPackageDao {

@Insert
void insertAppPackage(AppPackage appPackage);

@Query("SELECT package_name FROM app_packages WHERE [index]=:index")
String getPackageName(int index);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,25 @@ public class MessageLog {
private String notifRepliedMsg;
@ColumnInfo(name = "notif_reply_time")
private long notifReplyTime;
@ColumnInfo(name = "notif_event")
private String notifEvent;

public MessageLog(int index,
String notifTitle,
long notifArrivedTime,
String notifRepliedMsg,
long notifReplyTime
long notifReplyTime,
boolean notifIsReplied,
String notifEvent
) {
this.index = index;
this.notifId = null;
this.notifTitle = notifTitle;
this.notifArrivedTime = notifArrivedTime;
this.notifRepliedMsg = notifRepliedMsg;
this.notifReplyTime = notifReplyTime;
this.notifIsReplied = true;
this.notifIsReplied = notifIsReplied;
this.notifEvent = notifEvent;
}

public int getId() {
Expand Down Expand Up @@ -114,4 +119,17 @@ public long getNotifReplyTime() {
public void setNotifReplyTime(long notifReplyTime) {
this.notifReplyTime = notifReplyTime;
}

public String getNotifEvent() {
return notifEvent;
}

public void setNotifEvent(String notifEvent) {
this.notifEvent = notifEvent;
}

public String toString(){
return "" + id + "; " + index + "; " + notifId + "; " + notifTitle + "; " + notifArrivedTime + "; " + notifIsReplied + "; "
+ notifReplyTime + "; " + notifEvent;
}
}