diff --git a/app/build.gradle b/app/build.gradle index d018d27c9..fc76db8b2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -81,6 +81,8 @@ dependencies { annotationProcessor 'androidx.room:room-compiler:2.3.0' implementation "androidx.core:core-ktx:1.6.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0' implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.3.0' @@ -88,6 +90,7 @@ dependencies { implementation "com.squareup.okhttp3:logging-interceptor:4.7.2" implementation 'com.github.transferwise:sequence-layout:1.1.1' implementation "androidx.browser:browser:1.3.0" + implementation 'com.facebook.shimmer:shimmer:0.1.0@aar' } repositories { mavenCentral() diff --git a/app/schemas/com.parishod.watomatic.model.logs.MessageLogsDB/3.json b/app/schemas/com.parishod.watomatic.model.logs.MessageLogsDB/3.json new file mode 100644 index 000000000..e7b848ea7 --- /dev/null +++ b/app/schemas/com.parishod.watomatic.model.logs.MessageLogsDB/3.json @@ -0,0 +1,149 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "7e7d32963dbe325d983a4c069be65e75", + "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, 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 + } + ], + "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": [] + }, + { + "tableName": "supported_apps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`app_name` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`package_name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "app_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "package_name" + ], + "autoGenerate": false + }, + "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, '7e7d32963dbe325d983a4c069be65e75')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7242233a0..0aae04450 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,13 +6,12 @@ - + - - - - + + + + + ): RecyclerView.Adapter() { + lateinit var dbUtils: DbUtils + var newlyAddedApps: MutableList = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppsViewHolder { + val itemView = LayoutInflater.from(parent.context) + .inflate(R.layout.installed_apps_list, parent, false) + + dbUtils = DbUtils(parent.context) + newlyAddedApps = ArrayList(dbUtils.supportedApps) + + return AppsViewHolder(itemView) + } + + override fun onBindViewHolder(holder: AppsViewHolder, position: Int) { + holder.setData(installedAppsList[position]) + } + + override fun getItemCount(): Int { + return installedAppsList.size + } + + inner class AppsViewHolder(view: View) : RecyclerView.ViewHolder(view){ + + fun setData(app: App){ + try { + val icon: Drawable = itemView.context.packageManager.getApplicationIcon(app.packageName) + itemView.appIcon.setImageDrawable(icon) + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + val matrix = ColorMatrix() + matrix.setSaturation(0f) //0 means grayscale + val cf = ColorMatrixColorFilter(matrix) + itemView.appIcon.colorFilter = cf + } + itemView.appName.text = app.name + itemView.appNameCheckBox.tag = app + itemView.appNameCheckBox.isChecked = newlyAddedApps.contains(app) + itemView.appNameCheckBox.setOnClickListener{ + val view = it as CheckBox + if(view.isChecked){ + addToList(view.tag as App) + }else{ + removeFromList(view.tag as App) + } + } + itemView.setOnClickListener { + val view = itemView.appNameCheckBox as CheckBox + view.isChecked = !view.isChecked //Toggle checkbox + if(view.isChecked){ + addToList(view.tag as App) + }else{ + removeFromList(view.tag as App) + } + } + } + + fun addToList(app: App){ + if(!dbUtils.isPackageAlreadyAdded(app.packageName)) { + dbUtils.insertSupportedApp(app) + itemView.context?.let { + Toast.makeText(it, "${app.name} added to list.", Toast.LENGTH_SHORT).show() + } + } + } + + fun removeFromList(app: App){ + dbUtils.removeSupportedApp(app) + itemView.context?.let { + Toast.makeText(it, "${app.name} removed from list.", Toast.LENGTH_SHORT).show() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/parishod/watomatic/adapter/SupportedAppsAdapter.kt b/app/src/main/java/com/parishod/watomatic/adapter/SupportedAppsAdapter.kt index 85e431195..43b4ecd41 100644 --- a/app/src/main/java/com/parishod/watomatic/adapter/SupportedAppsAdapter.kt +++ b/app/src/main/java/com/parishod/watomatic/adapter/SupportedAppsAdapter.kt @@ -1,17 +1,21 @@ package com.parishod.watomatic.adapter +import android.content.Intent +import android.content.Intent.ACTION_VIEW import android.content.pm.PackageManager import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter import android.graphics.drawable.Drawable +import android.net.Uri import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.snackbar.Snackbar import com.google.android.material.switchmaterial.SwitchMaterial import com.parishod.watomatic.R -import com.parishod.watomatic.model.App +import com.parishod.watomatic.model.logs.App import com.parishod.watomatic.model.preferences.PreferencesManager import com.parishod.watomatic.model.utils.Constants import kotlinx.android.synthetic.main.supported_apps_list.view.* @@ -46,6 +50,7 @@ class SupportedAppsAdapter(private val listType: Constants.EnabledAppsDisplayTyp class AppsViewHolder(view: View) : RecyclerView.ViewHolder(view) { fun setData(app: App, listType: Constants.EnabledAppsDisplayType, onClickListener: View.OnClickListener?) { + var isAppInstalled = true; try { val icon: Drawable = itemView.context.packageManager.getApplicationIcon(app.packageName) itemView.appIcon.setImageDrawable(icon) @@ -56,6 +61,7 @@ class SupportedAppsAdapter(private val listType: Constants.EnabledAppsDisplayTyp } } catch (e: PackageManager.NameNotFoundException) { e.printStackTrace() + isAppInstalled = false if (listType == Constants.EnabledAppsDisplayType.VERTICAL) { val matrix = ColorMatrix() matrix.setSaturation(0f) //0 means grayscale @@ -63,11 +69,14 @@ class SupportedAppsAdapter(private val listType: Constants.EnabledAppsDisplayTyp itemView.appIcon.colorFilter = cf (itemView.appEnableSwitch as SwitchMaterial).setOnClickListener { - Toast.makeText(itemView.context, itemView.context.resources.getString(R.string.app_not_installed_text), Toast.LENGTH_SHORT).show() + showSnackBar(itemView, app.packageName, itemView.context.resources.getString(R.string.app_not_installed_text)) itemView.appEnableSwitch.isChecked = false + PreferencesManager.getPreferencesInstance(itemView.context).saveEnabledApps(itemView.appEnableSwitch.tag as App, itemView.appEnableSwitch.isChecked) } itemView.appIcon.setOnClickListener { - Toast.makeText(itemView.context, itemView.context.resources.getString(R.string.app_not_installed_text), Toast.LENGTH_SHORT).show() + showSnackBar(itemView, app.packageName, itemView.context.resources.getString(R.string.app_not_installed_text)) + itemView.appEnableSwitch.isChecked = false + PreferencesManager.getPreferencesInstance(itemView.context).saveEnabledApps(itemView.appEnableSwitch.tag as App, itemView.appEnableSwitch.isChecked) } } } @@ -76,21 +85,40 @@ class SupportedAppsAdapter(private val listType: Constants.EnabledAppsDisplayTyp itemView.appEnableSwitch.text = app.name itemView.appEnableSwitch.tag = app itemView.appEnableSwitch.isChecked = PreferencesManager.getPreferencesInstance(itemView.context).isAppEnabled(app) - itemView.appEnableSwitch.setOnCheckedChangeListener { buttonView, isChecked -> - val preferencesManager = PreferencesManager.getPreferencesInstance(itemView.context) - if (!isChecked && preferencesManager.enabledApps.size <= 1) { // Keep at-least one app selected - // Keep at-least one app selected - Toast.makeText( + if(isAppInstalled) { + (itemView.appEnableSwitch as SwitchMaterial).setOnClickListener { + val preferencesManager = + PreferencesManager.getPreferencesInstance(itemView.context) + if (!itemView.appEnableSwitch.isChecked && preferencesManager.enabledApps.size <= 1) { // Keep at-least one app selected + // Keep at-least one app selected + Toast.makeText( itemView.context, itemView.context.resources.getString(R.string.error_atleast_single_app_must_be_selected), Toast.LENGTH_SHORT - ).show() - buttonView.isChecked = true - } else { - preferencesManager.saveEnabledApps(buttonView.tag as App, isChecked) + ).show() + itemView.appEnableSwitch.isChecked = true + }else { + preferencesManager.saveEnabledApps( + itemView.appEnableSwitch.tag as App, + itemView.appEnableSwitch.isChecked + ) + } } } } } + + private fun showSnackBar(itemView: View, packageName: String, msg: String) { + val snackBar = Snackbar.make(itemView.rootView.findViewById(android.R.id.content), msg, Snackbar.LENGTH_LONG) + snackBar.setAction(itemView.context.resources.getString(R.string.install)) { + itemView.context.startActivity( + Intent( + ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=$packageName") + ) + ) + } + snackBar.show() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/parishod/watomatic/fragment/CustomAppsAdditionFragment.kt b/app/src/main/java/com/parishod/watomatic/fragment/CustomAppsAdditionFragment.kt new file mode 100644 index 000000000..6e6fbbee0 --- /dev/null +++ b/app/src/main/java/com/parishod/watomatic/fragment/CustomAppsAdditionFragment.kt @@ -0,0 +1,94 @@ +package com.parishod.watomatic.fragment + +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherApps +import android.content.pm.PackageManager +import android.os.Bundle +import android.os.UserManager +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.parishod.watomatic.R +import com.parishod.watomatic.adapter.InstalledAppsAdapter +import com.parishod.watomatic.model.logs.App +import com.parishod.watomatic.model.utils.Constants +import com.parishod.watomatic.model.utils.DbUtils +import kotlinx.android.synthetic.main.fragment_custom_apps.view.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.* +import kotlin.collections.ArrayList + +class CustomAppsAdditionFragment: Fragment(){ + lateinit var fragmentView: View + lateinit var installedApps: List + lateinit var installedAppsAdapter: InstalledAppsAdapter + val dbUtils: DbUtils by lazy { + DbUtils(context) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + fragmentView = inflater.inflate(R.layout.fragment_custom_apps, container, false) + fragmentView.shimmerFrameLayout.startShimmerAnimation() + + CoroutineScope(Dispatchers.Main).launch { + installedApps = getInstalledApps(context) + showInstalledApps() + } + + return fragmentView + } + + private fun showInstalledApps() { + fragmentView.shimmerFrameLayout.stopShimmerAnimation() + fragmentView.shimmerFrameLayout.visibility = View.GONE + + val layoutManager = LinearLayoutManager(context) + installedAppsAdapter = InstalledAppsAdapter(installedApps) + fragmentView.installedAppsList.layoutManager = layoutManager + fragmentView.installedAppsList.adapter = installedAppsAdapter + } + + private suspend fun getInstalledApps(context: Context?): List = + withContext(Dispatchers.Default) { + val apps: MutableList = ArrayList() + context?.let { + val pm: PackageManager = context.packageManager + val manager = context.getSystemService(Context.USER_SERVICE) as UserManager? + val launcher = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps? + + // Handle multi-profile support introduced in Android 5 + manager?.let { + launcher?.let { + for (profile in manager.userProfiles) { + for (activityInfo in launcher.getActivityList(null, profile)) { + val i = Intent(Intent.ACTION_MAIN) + i.component = activityInfo.componentName + i.addCategory(Intent.CATEGORY_LAUNCHER) + val resolveInfo = pm.resolveActivity(i, 0) + resolveInfo?.let { + if((resolveInfo.activityInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 1) { + val app = App(resolveInfo.loadLabel(pm) as String, resolveInfo.activityInfo.packageName)//AppInfo(resolveInfo.loadLabel(pm) as String, resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.loadIcon(pm)) + if(!Constants.SUPPORTED_APPS.contains(app) && !apps.contains(app)) { + apps.add(app) + } + } + } + } + } + } + } + } + return@withContext apps.sortedWith(compareBy { !ArrayList(dbUtils.supportedApps).contains(it) }.thenBy { it.name.lowercase() }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/parishod/watomatic/fragment/EnabledAppsFragment.kt b/app/src/main/java/com/parishod/watomatic/fragment/EnabledAppsFragment.kt index 46e0dfb7f..943cfaf2b 100644 --- a/app/src/main/java/com/parishod/watomatic/fragment/EnabledAppsFragment.kt +++ b/app/src/main/java/com/parishod/watomatic/fragment/EnabledAppsFragment.kt @@ -1,5 +1,6 @@ package com.parishod.watomatic.fragment +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -7,23 +8,56 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import com.parishod.watomatic.R +import com.parishod.watomatic.activity.customapp.CustomAppsAdditionActivity import com.parishod.watomatic.adapter.SupportedAppsAdapter -import com.parishod.watomatic.model.App +import com.parishod.watomatic.model.logs.App import com.parishod.watomatic.model.utils.Constants +import com.parishod.watomatic.model.utils.CustomDialog +import com.parishod.watomatic.model.utils.DbUtils import kotlinx.android.synthetic.main.fragment_enabled_apps.view.* class EnabledAppsFragment : Fragment() { - + private lateinit var fragmentView: View + private lateinit var dbUtils: DbUtils + private lateinit var supportedAppsAdapter: SupportedAppsAdapter override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val view: View = inflater.inflate(R.layout.fragment_enabled_apps, container, false) + fragmentView = inflater.inflate(R.layout.fragment_enabled_apps, container, false) val layoutManager = LinearLayoutManager(context) - val supportedAppsAdapter = SupportedAppsAdapter(Constants.EnabledAppsDisplayType.VERTICAL, ArrayList(Constants.SUPPORTED_APPS), null) - view.supportedAppsList.layoutManager = layoutManager - view.supportedAppsList.adapter = supportedAppsAdapter + dbUtils = DbUtils(context) + supportedAppsAdapter = SupportedAppsAdapter(Constants.EnabledAppsDisplayType.VERTICAL, ArrayList(dbUtils.supportedApps), null) + fragmentView.supportedAppsList.layoutManager = layoutManager + fragmentView.supportedAppsList.adapter = supportedAppsAdapter + + fragmentView.addCustomPackageButton.setOnClickListener { + val bundle = Bundle() + bundle.putString( + Constants.BETA_FEATURE_ALERT_DIALOG_TITLE, + getString(R.string.beta_feature_alert_dialog_title) + ) + bundle.putString( + Constants.BETA_FEATURE_ALERT_DIALOG_MSG, + getString(R.string.beta_feature_alert_dialog_msg) + ) + CustomDialog(activity).showDialog(bundle, null + ) { _, which -> + if (which == -2) { + //Decline + } else { + //Accept + startActivity(Intent(activity, CustomAppsAdditionActivity::class.java)) + } + } + } + return fragmentView + } + + override fun onResume() { + super.onResume() - return view + supportedAppsAdapter = SupportedAppsAdapter(Constants.EnabledAppsDisplayType.VERTICAL, ArrayList(dbUtils.supportedApps), null) + fragmentView.supportedAppsList.adapter = supportedAppsAdapter } } \ No newline at end of file diff --git a/app/src/main/java/com/parishod/watomatic/fragment/MainFragment.java b/app/src/main/java/com/parishod/watomatic/fragment/MainFragment.java index 72580138b..3b39e8cae 100644 --- a/app/src/main/java/com/parishod/watomatic/fragment/MainFragment.java +++ b/app/src/main/java/com/parishod/watomatic/fragment/MainFragment.java @@ -41,7 +41,7 @@ import com.parishod.watomatic.activity.enabledapps.EnabledAppsActivity; import com.parishod.watomatic.activity.settings.SettingsActivity; import com.parishod.watomatic.adapter.SupportedAppsAdapter; -import com.parishod.watomatic.model.App; +import com.parishod.watomatic.model.logs.App; import com.parishod.watomatic.model.CustomRepliesData; import com.parishod.watomatic.model.preferences.PreferencesManager; import com.parishod.watomatic.model.utils.Constants; @@ -81,6 +81,8 @@ public class MainFragment extends Fragment { private Activity mActivity; private SupportedAppsAdapter supportedAppsAdapter; private List enabledApps = new ArrayList<>(); + private Set supportedApps; + private DbUtils dbUtils; @Nullable @Override @@ -91,6 +93,16 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c mActivity = getActivity(); + dbUtils = new DbUtils(mActivity); + supportedApps = dbUtils.getSupportedApps(); + if(supportedApps.isEmpty()){ + supportedApps = Constants.SUPPORTED_APPS; + for (App app: supportedApps + ) { + dbUtils.insertSupportedApp(app); + } + } + customRepliesData = CustomRepliesData.getInstance(mActivity); preferencesManager = PreferencesManager.getPreferencesInstance(mActivity); @@ -188,8 +200,8 @@ private List getEnabledApps() { enabledApps.clear(); } enabledApps = new ArrayList<>(); - for (App app : Constants.SUPPORTED_APPS) { - if (preferencesManager.isAppEnabled(app)) { + for (App app : supportedApps) { + if(preferencesManager.isAppEnabled(app)){ enabledApps.add(app); } } @@ -282,7 +294,6 @@ public static boolean isAppInstalledFromStore(Context context) { } private boolean isAppUsedSufficientlyToAskRating() { - DbUtils dbUtils = new DbUtils(mActivity); long firstRepliedTime = dbUtils.getFirstRepliedTime(); return firstRepliedTime > 0 && System.currentTimeMillis() - firstRepliedTime > 2 * 24 * 60 * 60 * 1000L && dbUtils.getNunReplies() >= MIN_REPLIES_TO_ASK_APP_RATING; } diff --git a/app/src/main/java/com/parishod/watomatic/model/App.kt b/app/src/main/java/com/parishod/watomatic/model/App.kt deleted file mode 100644 index c43d93362..000000000 --- a/app/src/main/java/com/parishod/watomatic/model/App.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.parishod.watomatic.model - -data class App( - val name: String, - val packageName: String, -) \ No newline at end of file diff --git a/app/src/main/java/com/parishod/watomatic/model/logs/App.kt b/app/src/main/java/com/parishod/watomatic/model/logs/App.kt new file mode 100644 index 000000000..1bdf492b2 --- /dev/null +++ b/app/src/main/java/com/parishod/watomatic/model/logs/App.kt @@ -0,0 +1,11 @@ +package com.parishod.watomatic.model.logs + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "supported_apps") +data class App( + @ColumnInfo(name = "app_name") val name:String, + @PrimaryKey @ColumnInfo(name = "package_name") val packageName:String, +) \ No newline at end of file diff --git a/app/src/main/java/com/parishod/watomatic/model/logs/MessageLogsDB.java b/app/src/main/java/com/parishod/watomatic/model/logs/MessageLogsDB.java index f784bbeef..5583fef46 100644 --- a/app/src/main/java/com/parishod/watomatic/model/logs/MessageLogsDB.java +++ b/app/src/main/java/com/parishod/watomatic/model/logs/MessageLogsDB.java @@ -8,7 +8,7 @@ import com.parishod.watomatic.model.utils.Constants; -@Database(entities = {MessageLog.class, AppPackage.class}, version = 2) +@Database(entities = {MessageLog.class, AppPackage.class, App.class}, version = 3) public abstract class MessageLogsDB extends RoomDatabase { private static final String DB_NAME = Constants.LOGS_DB_NAME; private static MessageLogsDB _instance; @@ -26,4 +26,6 @@ public static synchronized MessageLogsDB getInstance(Context context) { public abstract MessageLogsDao logsDao(); public abstract AppPackageDao appPackageDao(); + + public abstract SupportedAppsDao supportedAppsDao(); } diff --git a/app/src/main/java/com/parishod/watomatic/model/logs/SupportedAppsDao.java b/app/src/main/java/com/parishod/watomatic/model/logs/SupportedAppsDao.java new file mode 100644 index 000000000..f66ccd837 --- /dev/null +++ b/app/src/main/java/com/parishod/watomatic/model/logs/SupportedAppsDao.java @@ -0,0 +1,20 @@ +package com.parishod.watomatic.model.logs; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.Query; + +import java.util.List; + +@Dao +public interface SupportedAppsDao { + @Query("SELECT * FROM supported_apps") + List getSupportedApps(); + @Insert + void insertSupportedApp(App app); + @Query("SELECT * FROM supported_apps WHERE package_name=:packageName") + App getAppData(String packageName); + @Delete + void removeSupportedApp(App app); +} diff --git a/app/src/main/java/com/parishod/watomatic/model/preferences/PreferencesManager.java b/app/src/main/java/com/parishod/watomatic/model/preferences/PreferencesManager.java index 285da77c4..cc561f540 100644 --- a/app/src/main/java/com/parishod/watomatic/model/preferences/PreferencesManager.java +++ b/app/src/main/java/com/parishod/watomatic/model/preferences/PreferencesManager.java @@ -9,9 +9,9 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.parishod.watomatic.R; -import com.parishod.watomatic.model.App; +import com.parishod.watomatic.model.logs.App; import com.parishod.watomatic.model.utils.AppUtils; -import com.parishod.watomatic.model.utils.Constants; +import com.parishod.watomatic.model.utils.DbUtils; import java.lang.reflect.Type; import java.util.Collection; @@ -69,7 +69,7 @@ private void init() { && !_sharedPrefs.contains(KEY_SELECTED_APPS_ARR); if (newInstall) { // Enable all supported apps for new install - setAppsAsEnabled(Constants.SUPPORTED_APPS); + setAppsAsEnabled(new DbUtils(thisAppContext).getSupportedApps()); // Set notifications ON for new installs setShowNotificationPref(true); @@ -124,7 +124,7 @@ public Set getEnabledApps() { // For upgrading users, preserve functionality by enabling only WhatsApp // (remove this when time most users would have updated. May be in 3 weeks after deploying this?) if (enabledAppsJsonStr == null || enabledAppsJsonStr.equals("[]")) { - enabledAppsJsonStr = setAppsAsEnabled(Collections.singleton(new App("WhatsApp", "com.whatsapp"))); + enabledAppsJsonStr = setAppsAsEnabled(Collections.singleton(new App( "WhatsApp", "com.whatsapp"))); } Type type = new TypeToken>() { diff --git a/app/src/main/java/com/parishod/watomatic/model/utils/Constants.kt b/app/src/main/java/com/parishod/watomatic/model/utils/Constants.kt index 0894b702d..180e69526 100644 --- a/app/src/main/java/com/parishod/watomatic/model/utils/Constants.kt +++ b/app/src/main/java/com/parishod/watomatic/model/utils/Constants.kt @@ -1,6 +1,6 @@ package com.parishod.watomatic.model.utils -import com.parishod.watomatic.model.App +import com.parishod.watomatic.model.logs.App object Constants { const val PERMISSION_DIALOG_TITLE = "permission_dialog_title" @@ -8,6 +8,8 @@ object Constants { const val PERMISSION_DIALOG_DENIED_TITLE = "permission_dialog_denied_title" const val PERMISSION_DIALOG_DENIED_MSG = "permission_dialog_denied_msg" const val PERMISSION_DIALOG_DENIED = "permission_dialog_denied" + const val BETA_FEATURE_ALERT_DIALOG_TITLE = "beta_feature_alert_dialog_title" + const val BETA_FEATURE_ALERT_DIALOG_MSG = "beta_feature_alert_dialog_msg" const val LOGS_DB_NAME = "logs_messages_db" const val NOTIFICATION_CHANNEL_ID = "watomatic" const val NOTIFICATION_CHANNEL_NAME = "watomatic_channel" diff --git a/app/src/main/java/com/parishod/watomatic/model/utils/CustomDialog.java b/app/src/main/java/com/parishod/watomatic/model/utils/CustomDialog.java index 4aa57a48b..33a7da5eb 100644 --- a/app/src/main/java/com/parishod/watomatic/model/utils/CustomDialog.java +++ b/app/src/main/java/com/parishod/watomatic/model/utils/CustomDialog.java @@ -60,6 +60,14 @@ public void showDialog(Bundle bundle, String type, DialogInterface.OnClickListen materialAlertDialogBuilder .setNegativeButton(mContext.getResources().getString(R.string.sure), onClickListener) .setPositiveButton(mContext.getResources().getString(R.string.retry), onClickListener); + } else if (bundle.containsKey(Constants.BETA_FEATURE_ALERT_DIALOG_TITLE)) { + materialAlertDialogBuilder = new MaterialAlertDialogBuilder(mContext) + .setTitle(bundle.getString(Constants.BETA_FEATURE_ALERT_DIALOG_TITLE)) + .setIcon(ContextCompat.getDrawable(mContext, R.drawable.ic_alert)) + .setMessage(bundle.getString(Constants.BETA_FEATURE_ALERT_DIALOG_MSG)); + materialAlertDialogBuilder + .setNegativeButton(mContext.getResources().getString(R.string.decline_auto_start_setting), onClickListener) + .setPositiveButton(mContext.getResources().getString(R.string.enable_auto_start_setting), onClickListener); } else { materialAlertDialogBuilder = new MaterialAlertDialogBuilder(mContext) .setTitle(bundle.getString(Constants.PERMISSION_DIALOG_TITLE)) diff --git a/app/src/main/java/com/parishod/watomatic/model/utils/DbUtils.java b/app/src/main/java/com/parishod/watomatic/model/utils/DbUtils.java index 463f0685a..837f52aec 100644 --- a/app/src/main/java/com/parishod/watomatic/model/utils/DbUtils.java +++ b/app/src/main/java/com/parishod/watomatic/model/utils/DbUtils.java @@ -4,10 +4,14 @@ import android.service.notification.StatusBarNotification; import com.parishod.watomatic.model.CustomRepliesData; +import com.parishod.watomatic.model.logs.App; import com.parishod.watomatic.model.logs.AppPackage; import com.parishod.watomatic.model.logs.MessageLog; import com.parishod.watomatic.model.logs.MessageLogsDB; +import java.util.HashSet; +import java.util.Set; + public class DbUtils { private final Context mContext; @@ -47,4 +51,24 @@ public long getFirstRepliedTime() { MessageLogsDB messageLogsDB = MessageLogsDB.getInstance(mContext.getApplicationContext()); return messageLogsDB.logsDao().getFirstRepliedTime(); } + + public Set getSupportedApps(){ + MessageLogsDB messageLogsDB = MessageLogsDB.getInstance(mContext.getApplicationContext()); + return new HashSet<>(messageLogsDB.supportedAppsDao().getSupportedApps()); + } + + public void insertSupportedApp(App app){ + MessageLogsDB messageLogsDB = MessageLogsDB.getInstance(mContext.getApplicationContext()); + messageLogsDB.supportedAppsDao().insertSupportedApp(app); + } + + public boolean isPackageAlreadyAdded(String packageName){ + MessageLogsDB messageLogsDB = MessageLogsDB.getInstance(mContext.getApplicationContext()); + return messageLogsDB.supportedAppsDao().getAppData(packageName) != null; + } + + public void removeSupportedApp(App app){ + MessageLogsDB messageLogsDB = MessageLogsDB.getInstance(mContext.getApplicationContext()); + messageLogsDB.supportedAppsDao().removeSupportedApp(app); + } } diff --git a/app/src/main/java/com/parishod/watomatic/model/utils/NotificationHelper.java b/app/src/main/java/com/parishod/watomatic/model/utils/NotificationHelper.java index b7fb7559e..3b9937a37 100644 --- a/app/src/main/java/com/parishod/watomatic/model/utils/NotificationHelper.java +++ b/app/src/main/java/com/parishod/watomatic/model/utils/NotificationHelper.java @@ -14,16 +14,20 @@ import com.parishod.watomatic.BuildConfig; import com.parishod.watomatic.R; import com.parishod.watomatic.activity.notification.NotificationIntentActivity; -import com.parishod.watomatic.model.App; +import com.parishod.watomatic.model.logs.App; import org.json.JSONException; import org.json.JSONObject; +import java.util.Set; + public class NotificationHelper { final private Context appContext; private static NotificationHelper _INSTANCE; private static NotificationManager notificationManager; private static final JSONObject appsList = new JSONObject(); + private Set supportedApps; + private DbUtils dbUtils; private NotificationHelper(Context appContext) { this.appContext = appContext; @@ -38,7 +42,9 @@ private void init() { notificationManager.createNotificationChannel(notificationChannel); } - for (App supportedApp : Constants.SUPPORTED_APPS) { + dbUtils = new DbUtils(appContext); + supportedApps = dbUtils.getSupportedApps(); + for (App supportedApp : supportedApps) { try { appsList.put(supportedApp.getPackageName(), false); } catch (JSONException e) { @@ -55,7 +61,7 @@ public static NotificationHelper getInstance(Context context) { } public void sendNotification(String title, String message, String packageName) { - for (App supportedApp : Constants.SUPPORTED_APPS) { + for (App supportedApp : supportedApps) { if (supportedApp.getPackageName().equalsIgnoreCase(packageName)) { title = supportedApp.getName() + ":" + title; break; @@ -79,7 +85,7 @@ public void sendNotification(String title, String message, String packageName) { //logic to detect if notifications exists else generate summary notification if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { StatusBarNotification[] notifications = notificationManager.getActiveNotifications(); - for (App supportedApp : Constants.SUPPORTED_APPS) { + for (App supportedApp : supportedApps) { try { appsList.put(supportedApp.getPackageName(), false); } catch (JSONException e) { diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 000000000..02c9464af --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus_24.xml b/app/src/main/res/drawable/ic_plus_24.xml new file mode 100644 index 000000000..70046c48f --- /dev/null +++ b/app/src/main/res/drawable/ic_plus_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_custom_apps.xml b/app/src/main/res/layout/activity_custom_apps.xml new file mode 100644 index 000000000..2cce64df0 --- /dev/null +++ b/app/src/main/res/layout/activity_custom_apps.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_custom_apps.xml b/app/src/main/res/layout/fragment_custom_apps.xml new file mode 100644 index 000000000..e4cf5abb1 --- /dev/null +++ b/app/src/main/res/layout/fragment_custom_apps.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_enabled_apps.xml b/app/src/main/res/layout/fragment_enabled_apps.xml index 42ac1f42c..ac463aa66 100644 --- a/app/src/main/res/layout/fragment_enabled_apps.xml +++ b/app/src/main/res/layout/fragment_enabled_apps.xml @@ -1,22 +1,39 @@ - - - + android:layout_height="match_parent"> + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/installed_apps_list.xml b/app/src/main/res/layout/installed_apps_list.xml new file mode 100644 index 000000000..05d93fa87 --- /dev/null +++ b/app/src/main/res/layout/installed_apps_list.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/shimmer_placeholder_layout.xml b/app/src/main/res/layout/shimmer_placeholder_layout.xml new file mode 100644 index 000000000..aeca69c2d --- /dev/null +++ b/app/src/main/res/layout/shimmer_placeholder_layout.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 406503ba6..29daf7dd2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -13,4 +13,5 @@ #FF018786 #FF000000 #FFFFFFFF + #E5E4E2 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 97f246f0e..b1d71cf50 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -190,6 +190,9 @@ of monthly goal Liberapay Paypal + CustomApps + Install + Add Custom Package(Beta) Add custom contact name Contact read access is required in order to filter replies by contact names. If the access is not enabled, only manually entered names will be allowed. Do you want to enable it? Enable contact permission @@ -203,4 +206,6 @@ Current donation progress Name cannot be blank Name already present + Warning! + This Feature is in Beta. Not tested to work with apps in the list and can cause unintended issues. \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..fae925cd3 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file