diff --git a/app/build.gradle b/app/build.gradle
index cfa9e535897b..33a7f72fae06 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -53,7 +53,8 @@ apply plugin: 'com.google.devtools.ksp'
println "Gradle uses Java ${Jvm.current()}"
-
+// apply MoEngage SDK for NMC
+apply from: "$rootProject.projectDir/nmc_moengage-dependencies.gradle"
configurations {
configureEach {
exclude group: 'org.jetbrains', module: 'annotations-java5' // via prism4j, already using annotations explicitly
@@ -430,6 +431,9 @@ dependencies {
// splash screen dependency ref: https://developer.android.com/develop/ui/views/launch/splash-screen/migrate
implementation 'androidx.core:core-splashscreen:1.0.1'
+
+ // NMC: dependency required to capture Advertising ID for Adjust & MoEngage SDK
+ implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1'
}
configurations.configureEach {
diff --git a/app/src/gplay/AndroidManifest.xml b/app/src/gplay/AndroidManifest.xml
index 34de04e2556d..aea73fc2d74a 100644
--- a/app/src/gplay/AndroidManifest.xml
+++ b/app/src/gplay/AndroidManifest.xml
@@ -70,6 +70,12 @@
+
+
+
diff --git a/app/src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java b/app/src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java
index ce6beea9b7e9..fbf4d585aa8c 100644
--- a/app/src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java
+++ b/app/src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java
@@ -13,6 +13,8 @@
import com.google.firebase.messaging.Constants.MessageNotificationKeys;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
+import com.moengage.firebase.MoEFireBaseHelper;
+import com.moengage.pushbase.MoEPushHelper;
import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.jobs.BackgroundJobManager;
import com.nextcloud.client.jobs.NotificationWork;
@@ -82,6 +84,12 @@ public void handleIntent(Intent intent) {
@Override
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
Log_OC.d(TAG, "onMessageReceived");
+ // NMC: check and pass the Notification payload to MoEngage to handle it
+ if (MoEPushHelper.getInstance().isFromMoEngagePlatform(remoteMessage.getData())) {
+ MoEFireBaseHelper.getInstance().passPushPayload(getApplicationContext(), remoteMessage.getData());
+ return;
+ }
+
final Map data = remoteMessage.getData();
final String subject = data.get(NotificationWork.KEY_NOTIFICATION_SUBJECT);
final String signature = data.get(NotificationWork.KEY_NOTIFICATION_SIGNATURE);
diff --git a/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt b/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt
index ebcb5f7dd4d3..e0958602f541 100644
--- a/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt
+++ b/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt
@@ -23,6 +23,7 @@ import com.nextcloud.client.core.Clock
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.common.NextcloudClient
import com.owncloud.android.MainApp
+import com.nmc.android.marketTracking.MoEngageSdkUtils
import com.owncloud.android.R
import com.owncloud.android.datamodel.ArbitraryDataProvider
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
@@ -142,6 +143,8 @@ class AccountRemovalWork(
if (userRemoved) {
eventBus.post(AccountRemovedEvent())
+ // NMC: track user logout
+ MoEngageSdkUtils.trackUserLogout(context)
}
return Result.success()
diff --git a/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt b/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt
index 82fdbaab166b..d5d327e548be 100644
--- a/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt
+++ b/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt
@@ -21,6 +21,7 @@ import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap
import com.nextcloud.client.account.User
+import com.nmc.android.marketTracking.MoEngageSdkUtils
import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.SyncedFolderProvider
@@ -91,6 +92,9 @@ class ShortcutUtil @Inject constructor(private val mContext: Context) {
pinShortcutInfo,
successCallback.intentSender
)
+
+ // NMC: track pin to home screen event
+ MoEngageSdkUtils.trackPinHomeScreenEvent(mContext, file)
}
}
diff --git a/app/src/main/java/com/nmc/android/marketTracking/MoEngagePropertiesHelper.kt b/app/src/main/java/com/nmc/android/marketTracking/MoEngagePropertiesHelper.kt
new file mode 100644
index 000000000000..c74ec8af33c0
--- /dev/null
+++ b/app/src/main/java/com/nmc/android/marketTracking/MoEngagePropertiesHelper.kt
@@ -0,0 +1,26 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Your Name
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nmc.android.marketTracking
+
+enum class EventFileType(val fileType: String) {
+ PHOTO("foto"),
+ SCAN("scan"),
+ VIDEO("video"),
+ AUDIO("audio"),
+ TEXT("text"),
+ PDF("pdf"),
+ DOCUMENT("docx"),
+ SPREADSHEET("xlsx"),
+ PRESENTATION("pptx"),
+ OTHER("other"), // default
+}
+
+enum class EventFolderType(val folderType: String) {
+ ENCRYPTED("encrypted"),
+ NOT_ENCRYPTED("not encrypted")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt
new file mode 100644
index 000000000000..c19ed45f456b
--- /dev/null
+++ b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt
@@ -0,0 +1,475 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Your Name
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nmc.android.marketTracking
+
+import android.Manifest
+import android.app.Application
+import android.content.Context
+import android.os.Build
+import com.moengage.core.DataCenter
+import com.moengage.core.MoECoreHelper
+import com.moengage.core.MoEngage
+import com.moengage.core.Properties
+import com.moengage.core.analytics.MoEAnalyticsHelper
+import com.moengage.core.config.NotificationConfig
+import com.moengage.core.enableAdIdTracking
+import com.moengage.core.enableAndroidIdTracking
+import com.moengage.core.model.AppStatus
+import com.moengage.inapp.MoEInAppHelper
+import com.moengage.pushbase.MoEPushHelper
+import com.nextcloud.client.account.User
+import com.nextcloud.common.NextcloudClient
+import com.nextcloud.utils.extensions.getFormattedStringDate
+import com.nmc.android.utils.FileUtils
+import com.owncloud.android.BuildConfig
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.datamodel.Template
+import com.owncloud.android.lib.common.OwnCloudClientFactory
+import com.owncloud.android.lib.common.Quota
+import com.owncloud.android.lib.common.UserInfo
+import com.owncloud.android.lib.common.accounts.AccountUtils
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation
+import com.owncloud.android.utils.MimeTypeUtil
+import com.owncloud.android.utils.PermissionUtil
+import kotlin.math.ceil
+import kotlin.math.floor
+import kotlin.math.round
+
+object MoEngageSdkUtils {
+
+ private const val USER_PROPERTIES__STORAGE_CAPACITY = "storage_capacity" // in GB
+ private const val USER_PROPERTIES__STORAGE_USED = "storage_used" // % of storage used
+ private const val USER_PROPERTIES__CONTACT_BACKUP = "contact_backup_on"
+ private const val USER_PROPERTIES__AUTO_UPLOAD = "auto_upload_on"
+ private const val USER_PROPERTIES__APP_VERSION = "app_version"
+
+ private const val EVENT__ACTION_BUTTON = "action_button_clicked" // when user clicks on fab + button
+ private const val EVENT__UPLOAD_FILE =
+ "upload_file" // when user uploads any file (not applicable for folder) from other apps
+ private const val EVENT__CREATE_FILE = "create_file" // when user creates any file in app
+ private const val EVENT__CREATE_FOLDER = "create_folder"
+ private const val EVENT__ADD_FAVORITE = "add_favorite"
+ private const val EVENT__SHARE_FILE = "share_file" // when user share any file using link
+ private const val EVENT__OFFLINE_AVAILABLE = "offline_available"
+ private const val EVENT__PIN_TO_HOME_SCREEN = "pin_to_homescreen"
+ private const val EVENT__ONLINE_OFFICE_USED = "online_office_used" // when user opens any office files
+
+ // screen view events when user open specific screen
+ private const val SCREEN_EVENT__FAVOURITES = "favorites"
+ private const val SCREEN_EVENT__MEDIA = "medien"
+ private const val SCREEN_EVENT__OFFLINE_FILES = "offline_files"
+ private const val SCREEN_EVENT__SHARED = "shared"
+ private const val SCREEN_EVENT__DELETED_FILES = "deleted_files"
+ private const val SCREEN_EVENT__NOTIFICATIONS = "notifications"
+
+ // properties attributes key
+ private const val PROPERTIES__FILE_TYPE = "file_type"
+ private const val PROPERTIES__FOLDER_TYPE = "folder_type"
+ private const val PROPERTIES__FILE_SIZE = "file_size" // in MB
+ private const val PROPERTIES__CREATION_DATE = "creation_date" // yyyy-MM-dd
+ private const val PROPERTIES__UPLOAD_DATE = "upload_date" // // yyyy-MM-dd
+
+ private const val KILOBYTE: Long = 1024
+ private const val MEGABYTE = KILOBYTE * 1024
+ private const val GIGABYTE = MEGABYTE * 1024
+
+ // app version code for which user attributes need to track
+ // this should be the previous version before MoEngage is included
+ // Note: will be removed in future once MoEngage feature rolled out to all devices
+ private const val OLD_VERSION_CODE = 7_29_00
+
+ private const val DATE_FORMAT = "yyyy-MM-dd"
+
+ // maximum post notification permission retry count
+ private const val PUSH_PERMISSION_REQUEST_RETRY_COUNT = 2
+
+ @JvmStatic
+ fun initMoEngageSDK(application: Application) {
+ val moEngage = MoEngage.Builder(application, BuildConfig.MOENGAGE_APP_ID, DataCenter.DATA_CENTER_2)
+ .configureNotificationMetaData(
+ NotificationConfig(
+ R.drawable.notification_icon,
+ R.drawable.notification_icon,
+ R.color.primary,
+ false
+ )
+ )
+ .build()
+ MoEngage.initialiseDefaultInstance(moEngage)
+
+ updatePostNotificationsPermission(application)
+
+ enableDeviceIdentifierTracking(application)
+
+ // track app version at app launch
+ trackAppVersion(application)
+ }
+
+ // for NMC the default privacy tracking consent is always taken from users
+ // so the tracking will always be enabled for MoEngage
+ private fun enableDeviceIdentifierTracking(context: Context) {
+ enableAndroidIdTracking(context)
+ enableAdIdTracking(context)
+ }
+
+ private fun trackAppVersion(context: Context) {
+ MoEAnalyticsHelper.setUserAttribute(context, USER_PROPERTIES__APP_VERSION, BuildConfig.VERSION_NAME)
+ }
+
+ /**
+ * method to check if a user updated the app from older version where MoEngage was not included
+ * if user app version is old and is logged in then we have to auto capture the user attributes to map the events
+ * Note: Will be removed when MoEngage will be rolled out to all versions
+ */
+ @JvmStatic
+ fun captureUserAttrsForOldAppVersion(
+ context: Context,
+ lastSeenVersionCode: Int,
+ user: User
+ ) {
+ if (lastSeenVersionCode in 1..OLD_VERSION_CODE && !user.isAnonymous) {
+ fetchUserInfo(context, user)
+ }
+
+ // if user is not logged in for older app versions then nothing to do
+ // as the events will be captured after successful login
+ }
+
+ @JvmStatic
+ fun trackAppInstallOrUpdate(context: Context, lastSeenVersionCode: Int) {
+ if (lastSeenVersionCode <= 0) {
+ trackAppInstall(context)
+ } else if (lastSeenVersionCode < BuildConfig.VERSION_CODE) {
+ trackAppUpdate(context)
+ }
+ // For same version code no event has to send
+ }
+
+ private fun trackAppInstall(context: Context) {
+ // For Fresh Install of App
+ MoEAnalyticsHelper.setAppStatus(context, AppStatus.INSTALL)
+ }
+
+ private fun trackAppUpdate(context: Context) {
+ // For Existing user who has updated the app
+ MoEAnalyticsHelper.setAppStatus(context, AppStatus.UPDATE)
+ }
+
+ @JvmStatic
+ fun trackUserLogin(context: Context, userInfo: UserInfo) {
+ userInfo.id?.let {
+ MoEAnalyticsHelper.setUniqueId(context, it)
+ }
+ userInfo.displayName?.let {
+ MoEAnalyticsHelper.setUserName(context, it)
+ }
+ userInfo.email?.let {
+ MoEAnalyticsHelper.setEmailId(context, it)
+ }
+ trackQuotaStorage(context, userInfo.quota)
+ }
+
+ @JvmStatic
+ fun trackQuotaStorage(context: Context, quota: Quota?) {
+ quota?.let {
+ val totalQuota = if (it.quota > 0) {
+ bytesToGB(it.total).toString()
+ } else {
+ it.total.toString()
+ }
+ // capture storage capacity
+ MoEAnalyticsHelper.setUserAttribute(context, USER_PROPERTIES__STORAGE_CAPACITY, totalQuota)
+
+ val usedSpace = ceil(it.relative).toInt()
+ // capture storage used
+ MoEAnalyticsHelper.setUserAttribute(context, USER_PROPERTIES__STORAGE_USED, usedSpace)
+ }
+ }
+
+ @JvmStatic
+ fun trackContactBackup(context: Context, isEnabled: Boolean) {
+ MoEAnalyticsHelper.setUserAttribute(context, USER_PROPERTIES__CONTACT_BACKUP, isEnabled)
+ }
+
+ @JvmStatic
+ fun trackAutoUpload(context: Context, syncedFoldersCount: Int) {
+ // since multiple folders can be enabled for auto upload
+ // user can add or remove a folder anytime, and we don't have single flag to check if auto upload is enabled
+ // so we have to check the count and if there are folders more than 0 i.e. auto upload is enabled
+ MoEAnalyticsHelper.setUserAttribute(context, USER_PROPERTIES__AUTO_UPLOAD, syncedFoldersCount > 0)
+ }
+
+ @JvmStatic
+ fun trackActionButtonEvent(context: Context) {
+ MoEAnalyticsHelper.trackEvent(context, EVENT__ACTION_BUTTON, Properties())
+ }
+
+ @JvmStatic
+ fun trackUploadFileEvent(context: Context, file: OCFile, originalStoragePath: String) {
+ if (file.isFolder) return
+
+ MoEAnalyticsHelper.trackEvent(
+ context, EVENT__UPLOAD_FILE, getCommonProperties(
+ file,
+ FileUtils.isScannedFiles(context, originalStoragePath)
+ )
+ )
+ }
+
+ @JvmStatic
+ fun trackCreateFileEvent(context: Context, file: OCFile, type: Template.Type? = null) {
+ if (file.isFolder) return
+
+ val properties = Properties()
+ properties.addAttribute(PROPERTIES__FILE_TYPE, getOfficeFileType(type) { getFileType(file) }.fileType)
+ properties.addAttribute(PROPERTIES__FILE_SIZE, bytesToMBInDecimal(file.fileLength).toString())
+ properties.addAttribute(
+ PROPERTIES__CREATION_DATE,
+ // using modification timestamp as this will always have value
+ file.modificationTimestamp.getFormattedStringDate(DATE_FORMAT)
+ )
+
+ MoEAnalyticsHelper.trackEvent(context, EVENT__CREATE_FILE, properties)
+ }
+
+ @JvmStatic
+ fun trackCreateFolderEvent(context: Context, file: OCFile) {
+ if (!file.isFolder) return
+
+ val properties = Properties()
+ properties.addAttribute(PROPERTIES__FOLDER_TYPE, getFolderType(file).folderType)
+ properties.addAttribute(
+ PROPERTIES__CREATION_DATE,
+ // using modification timestamp because for folder creationTimeStamp is always 0
+ file.modificationTimestamp.getFormattedStringDate(DATE_FORMAT)
+ )
+
+ MoEAnalyticsHelper.trackEvent(context, EVENT__CREATE_FOLDER, properties)
+ }
+
+ @JvmStatic
+ fun trackAddFavoriteEvent(context: Context, file: OCFile) {
+ if (file.isFolder) return
+
+ MoEAnalyticsHelper.trackEvent(context, EVENT__ADD_FAVORITE, getCommonProperties(file))
+ }
+
+ @JvmStatic
+ fun trackShareFileEvent(context: Context, file: OCFile) {
+ if (file.isFolder) return
+
+ MoEAnalyticsHelper.trackEvent(context, EVENT__SHARE_FILE, getCommonProperties(file))
+ }
+
+ @JvmStatic
+ fun trackOfflineAvailableEvent(context: Context, file: OCFile) {
+ if (file.isFolder) return
+
+ MoEAnalyticsHelper.trackEvent(context, EVENT__OFFLINE_AVAILABLE, getCommonProperties(file))
+ }
+
+ @JvmStatic
+ fun trackPinHomeScreenEvent(context: Context, file: OCFile) {
+ if (file.isFolder) return
+
+ MoEAnalyticsHelper.trackEvent(context, EVENT__PIN_TO_HOME_SCREEN, getCommonProperties(file))
+ }
+
+ @JvmStatic
+ fun trackOnlineOfficeUsedEvent(context: Context, file: OCFile) {
+ if (file.isFolder) return
+
+ MoEAnalyticsHelper.trackEvent(context, EVENT__ONLINE_OFFICE_USED, Properties())
+ }
+
+ @JvmStatic
+ fun trackFavouriteScreenEvent(context: Context) {
+ MoEAnalyticsHelper.trackEvent(context, SCREEN_EVENT__FAVOURITES, Properties())
+ }
+
+ @JvmStatic
+ fun trackMediaScreenEvent(context: Context) {
+ MoEAnalyticsHelper.trackEvent(context, SCREEN_EVENT__MEDIA, Properties())
+ }
+
+ @JvmStatic
+ fun trackOfflineFilesScreenEvent(context: Context) {
+ MoEAnalyticsHelper.trackEvent(context, SCREEN_EVENT__OFFLINE_FILES, Properties())
+ }
+
+ @JvmStatic
+ fun trackSharedScreenEvent(context: Context) {
+ MoEAnalyticsHelper.trackEvent(context, SCREEN_EVENT__SHARED, Properties())
+ }
+
+ @JvmStatic
+ fun trackDeletedFilesScreenEvent(context: Context) {
+ MoEAnalyticsHelper.trackEvent(context, SCREEN_EVENT__DELETED_FILES, Properties())
+ }
+
+ @JvmStatic
+ fun trackNotificationsScreenEvent(context: Context) {
+ MoEAnalyticsHelper.trackEvent(context, SCREEN_EVENT__NOTIFICATIONS, Properties())
+ }
+
+ @JvmStatic
+ fun trackUserLogout(context: Context) {
+ MoECoreHelper.logoutUser(context)
+ }
+
+ private fun getCommonProperties(file: OCFile, isScan: Boolean = false): Properties {
+ val properties = Properties()
+ properties.addAttribute(PROPERTIES__FILE_TYPE, getFileType(file, isScan).fileType)
+ properties.addAttribute(PROPERTIES__FILE_SIZE, bytesToMBInDecimal(file.fileLength).toString())
+ properties.addAttribute(
+ PROPERTIES__CREATION_DATE,
+ // using modification timestamp as this will always have value
+ file.modificationTimestamp.getFormattedStringDate(DATE_FORMAT)
+ )
+ properties.addAttribute(PROPERTIES__UPLOAD_DATE, file.uploadTimestamp.getFormattedStringDate(DATE_FORMAT))
+ return properties
+ }
+
+ private fun bytesToGB(bytes: Long): Int {
+ return floor((bytes / GIGABYTE).toDouble()).toInt()
+ }
+
+ private fun bytesToMBInDecimal(bytes: Long): Double {
+ val mb = bytes.toDouble() / MEGABYTE
+ return round((mb * 10)) / 10 // Round down to 1 decimal place
+ }
+
+ private fun getFileType(file: OCFile, isScan: Boolean = false): EventFileType {
+ // if upload is happening through scan then no need to check mime type
+ // just set SCAN as type and send event
+ if (isScan) return EventFileType.SCAN
+
+ return when {
+ MimeTypeUtil.isImage(file) -> {
+ EventFileType.PHOTO
+ }
+
+ MimeTypeUtil.isVideo(file) -> {
+ EventFileType.VIDEO
+ }
+
+ MimeTypeUtil.isAudio(file) -> {
+ EventFileType.AUDIO
+ }
+
+ MimeTypeUtil.isPDF(file) -> {
+ EventFileType.PDF
+ }
+
+ MimeTypeUtil.isText(file) -> {
+ EventFileType.TEXT
+ }
+
+ else -> {
+ EventFileType.OTHER
+ }
+ }
+ }
+
+ private fun getOfficeFileType(
+ type: Template.Type?,
+ getFileType: () -> EventFileType
+ ): EventFileType {
+ return when (type) {
+ Template.Type.DOCUMENT -> {
+ EventFileType.DOCUMENT
+ }
+
+ Template.Type.SPREADSHEET -> {
+ EventFileType.SPREADSHEET
+ }
+
+ Template.Type.PRESENTATION -> {
+ EventFileType.PRESENTATION
+ }
+
+ else -> {
+ getFileType()
+ }
+ }
+ }
+
+ private fun getFolderType(file: OCFile): EventFolderType {
+ return if (file.isEncrypted) {
+ EventFolderType.ENCRYPTED
+ } else {
+ EventFolderType.NOT_ENCRYPTED
+ }
+ }
+
+ private fun fetchUserInfo(context: Context, user: User) {
+ val t = Thread(Runnable {
+ val nextcloudClient: NextcloudClient
+ try {
+ nextcloudClient = OwnCloudClientFactory.createNextcloudClient(
+ user,
+ context
+ )
+ } catch (e: AccountUtils.AccountNotFoundException) {
+ Log_OC.e(this, "Error retrieving user info", e)
+ return@Runnable
+ } catch (e: SecurityException) {
+ Log_OC.e(this, "Error retrieving user info", e)
+ return@Runnable
+ }
+
+ val result = GetUserInfoRemoteOperation().execute(nextcloudClient)
+ if (result.isSuccess && result.resultData != null) {
+ val userInfo = result.resultData
+
+ trackUserLogin(context, userInfo)
+ } else {
+ Log_OC.d(this, result.logMessage)
+ }
+ })
+
+ t.start()
+ }
+
+ @JvmStatic
+ fun updatePostNotificationsPermission(context: Context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val isGranted = PermissionUtil.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
+
+ MoEPushHelper.getInstance().pushPermissionResponse(context, isGranted)
+
+ if (!isGranted) {
+ MoEPushHelper.getInstance()
+ .updatePushPermissionRequestCount(context, PUSH_PERMISSION_REQUEST_RETRY_COUNT)
+ }
+ } else {
+ MoEPushHelper.getInstance().setUpNotificationChannels(context)
+ }
+ }
+
+ /**
+ * function should be called from onStart() of Activity
+ * or onResume() of Fragment
+ */
+ @JvmStatic
+ fun displayInAppNotification(context: Context) {
+ MoEInAppHelper.getInstance().showInApp(context)
+ }
+
+ /**
+ * To show In-App in both Portrait and Landscape mode properly
+ * when Activity is handling Config changes by itself
+ * call this function from onConfigurationChanged()
+ */
+ @JvmStatic
+ fun handleConfigChangesForInAppNotification() {
+ MoEInAppHelper.getInstance().onConfigurationChanged()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nmc/android/utils/FileUtils.java b/app/src/main/java/com/nmc/android/utils/FileUtils.java
new file mode 100644
index 000000000000..97496b9a5f59
--- /dev/null
+++ b/app/src/main/java/com/nmc/android/utils/FileUtils.java
@@ -0,0 +1,171 @@
+package com.nmc.android.utils;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Environment;
+import android.text.TextUtils;
+
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.ui.helpers.FileOperationsHelper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import androidx.annotation.NonNull;
+
+// TODO: 06/24/23 Migrate to FileUtil once Rotate PR is upstreamed and merged by NC
+public class FileUtils {
+ private static final String TAG = FileUtils.class.getSimpleName();
+
+ private static final String SCANS_FILE_DIR = "Scans";
+ private static final String SCANNED_FILE_PREFIX = "scan_";
+
+ // while generating pdf using Scanbot it provide us following path:
+ // /scanbot-sdk/snapping_documents/.pdf
+ // this path will help us to differentiate if pdf file is generating by scanbot
+ private static final String SCANBOT_PDF_LOCAL_PATH = "/scanbot-sdk/snapping_documents/";
+ private static final int JPG_FILE_TYPE = 1;
+ private static final int PNG_FILE_TYPE = 2;
+
+ public static File saveJpgImage(Context context, Bitmap bitmap, String imageName, int quality) {
+ return createFileAndSaveImage(context, bitmap, imageName, quality, JPG_FILE_TYPE);
+ }
+
+ public static File savePngImage(Context context, Bitmap bitmap, String imageName, int quality) {
+ return createFileAndSaveImage(context, bitmap, imageName, quality, PNG_FILE_TYPE);
+ }
+
+ private static File createFileAndSaveImage(Context context, Bitmap bitmap, String imageName, int quality,
+ int fileType) {
+ File file = fileType == PNG_FILE_TYPE ? getPngImageName(context, imageName) : getJpgImageName(context,
+ imageName);
+ return saveImage(file, bitmap, quality, fileType);
+ }
+
+ private static File saveImage(File file, Bitmap bitmap, int quality, int fileType) {
+ try {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.JPEG, quality, bos);
+ byte[] bitmapData = bos.toByteArray();
+
+ FileOutputStream fileOutputStream = new FileOutputStream(file);
+ fileOutputStream.write(bitmapData);
+ fileOutputStream.flush();
+ fileOutputStream.close();
+ return file;
+ } catch (Exception e) {
+ Log_OC.e(TAG, " Failed to save image : " + e.getLocalizedMessage());
+ return null;
+ }
+ }
+
+ private static File getJpgImageName(Context context, String imageName) {
+ File imageFile = getOutputMediaFile(context);
+ if (!TextUtils.isEmpty(imageName)) {
+ return new File(imageFile.getPath() + File.separator + imageName + ".jpg");
+ } else {
+ return new File(imageFile.getPath() + File.separator + "IMG_" + FileOperationsHelper.getCapturedImageName());
+ }
+ }
+
+ private static File getPngImageName(Context context, String imageName) {
+ File imageFile = getOutputMediaFile(context);
+ if (!TextUtils.isEmpty(imageName)) {
+ return new File(imageFile.getPath() + File.separator + imageName + ".png");
+ } else {
+ return new File(imageFile.getPath() + File.separator + "IMG_" + FileOperationsHelper.getCapturedImageName().replace(".jpg", ".png"));
+ }
+ }
+
+ private static File getTextFileName(Context context, String fileName) {
+ File txtFileName = getOutputMediaFile(context);
+ if (!TextUtils.isEmpty(fileName)) {
+ return new File(txtFileName.getPath() + File.separator + fileName + ".txt");
+ } else {
+ return new File(txtFileName.getPath() + File.separator + FileOperationsHelper.getCapturedImageName().replace(".jpg", ".txt"));
+ }
+ }
+
+ public static File getPdfFileName(Context context, String fileName) {
+ File pdfFileName = getOutputMediaFile(context);
+ if (!TextUtils.isEmpty(fileName)) {
+ return new File(pdfFileName.getPath() + File.separator + fileName + ".pdf");
+ } else {
+ return new File(pdfFileName.getPath() + File.separator + FileOperationsHelper.getCapturedImageName().replace(".jpg", ".pdf"));
+ }
+ }
+
+ public static String scannedFileName() {
+ return SCANNED_FILE_PREFIX + new SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US).format(new Date());
+ }
+
+ public static File getOutputMediaFile(Context context) {
+ File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), SCANS_FILE_DIR);
+ if (!file.exists()) {
+ file.mkdir();
+ }
+ return file;
+ }
+
+ public static Bitmap convertFileToBitmap(File file) {
+ String filePath = file.getPath();
+ Bitmap bitmap = BitmapFactory.decodeFile(filePath);
+ return bitmap;
+ }
+
+ public static File writeTextToFile(Context context, String textToWrite, String fileName) {
+ File file = getTextFileName(context, fileName);
+ try {
+ FileWriter fileWriter = new FileWriter(file);
+ fileWriter.write(textToWrite);
+ fileWriter.flush();
+ fileWriter.close();
+ return file;
+ } catch (IOException e) {
+ //e.printStackTrace();
+ Log_OC.e(TAG, "Failed to write file : " + e.toString());
+ }
+ return null;
+
+ }
+
+ /**
+ * method to check if uploading file is from Scans or not
+ *
+ * @param path local path of the uploading file
+ */
+ public static boolean isScannedFiles(@NonNull Context context, @NonNull String path) {
+ if (path.isEmpty()) {
+ return false;
+ }
+
+ return (path.contains(getOutputMediaFile(context).getPath()) || path.contains(SCANBOT_PDF_LOCAL_PATH));
+ }
+
+ /**
+ * delete all the files inside the pictures directory
+ * this directory is getting used to store the scanned images temporarily till they uploaded to cloud
+ * the scanned files after downloading will get deleted by UploadWorker but in case some files still there
+ * then we have to delete it when user do logout from the app
+ * @param context
+ */
+ public static void deleteFilesFromPicturesDirectory(Context context) {
+ File getFileDirectory = getOutputMediaFile(context);
+ if (getFileDirectory.isDirectory()) {
+ File[] fileList = getFileDirectory.listFiles();
+ if (fileList != null && fileList.length > 0) {
+ for (File file : fileList) {
+ file.delete();
+ }
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java
index 95ffbcd0a614..70bb3a0e90ac 100644
--- a/app/src/main/java/com/owncloud/android/MainApp.java
+++ b/app/src/main/java/com/owncloud/android/MainApp.java
@@ -63,6 +63,7 @@
import com.nextcloud.receiver.NetworkChangeReceiver;
import com.nextcloud.utils.extensions.ContextExtensionsKt;
import com.nextcloud.utils.mdm.MDMConfig;
+import com.nmc.android.marketTracking.MoEngageSdkUtils;
import com.nmc.android.ui.LauncherActivity;
import com.owncloud.android.authentication.PassCodeManager;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
@@ -313,6 +314,9 @@ public void onCreate() {
registerActivityLifecycleCallbacks(new ActivityInjector());
+ // NMC: init MoEngage SDK
+ initMoEngage();
+ // NMC: end
//update the app restart count when app is launched by the user
inAppReviewHelper.resetAndIncrementAppRestartCounter();
@@ -992,6 +996,13 @@ private static void cleanOldEntries(Clock clock) {
}
}
+ private void initMoEngage(){
+ MoEngageSdkUtils.initMoEngageSDK(this);
+ MoEngageSdkUtils.trackAppInstallOrUpdate(this, preferences.getLastSeenVersionCode());
+ MoEngageSdkUtils.captureUserAttrsForOldAppVersion(this, preferences.getLastSeenVersionCode(),
+ accountManager.getUser());
+ }
+
@Override
public AndroidInjector