Loading app/build.gradle +14 −4 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ plugins { def versionMajor = 1 def versionMinor = 9 def versionPatch = 2 def versionPatch = 3 def getTestProp(String propName) { def result = "" Loading Loading @@ -39,7 +39,7 @@ android { compileSdk = 36 defaultConfig { applicationId = "foundation.e.drive" applicationId = "foundation.e.drive.ng" minSdk = 26 targetSdk = 36 versionCode = versionMajor * 1000000 + versionMinor * 1000 + versionPatch Loading @@ -49,6 +49,8 @@ android { manifestPlaceholders = [ 'appAuthRedirectScheme': applicationId, ] resValue "string", "media_sync_provider_authority", "${applicationId}.providers.MediasSyncProvider" } splits { Loading @@ -67,18 +69,26 @@ android { keyAlias = 'platform' keyPassword = 'android' } if (rootProject.file("../murena-test.jks").exists()) { murenaTest { storeFile = rootProject.file("../murena-test.jks") storePassword = 'murena' keyAlias = 'murena' keyPassword = 'murena' } } } buildTypes { release { minifyEnabled = false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig = signingConfigs.debugConfig signingConfig = signingConfigs.findByName('murenaTest') ?: signingConfigs.debugConfig buildConfigField("String", "SENTRY_DSN", "\"${getSentryDsn()}\"") } debug { signingConfig = signingConfigs.debugConfig signingConfig = signingConfigs.findByName('murenaTest') ?: signingConfigs.debugConfig buildConfigField("String", "SENTRY_DSN", "\"dummy\"") } } Loading app/src/main/AndroidManifest.xml +44 −7 Original line number Diff line number Diff line Loading @@ -27,7 +27,11 @@ tools:ignore="QueryAllPackagesPermission" /> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="foundation.e.accountmanager.permission.ACCOUNT_EVENTS"/> <uses-permission android:name="foundation.e.accountmanager.ng.permission.ACCOUNT_EVENTS"/> <queries> <package android:name="foundation.e.accountmanager.ng" /> </queries> <application android:name=".EdriveApplication" Loading @@ -43,6 +47,17 @@ android:label="@string/my_account" android:theme="@style/AccountActivityTheme" /> <activity android:name=".account.DrivePermissionRequestActivity" android:exported="true" android:permission="foundation.e.accountmanager.ng.permission.ACCOUNT_EVENTS" android:theme="@android:style/Theme.Translucent.NoTitleBar"> <intent-filter> <action android:name="foundation.e.accountmanager.action.REQUEST_DRIVE_PERMISSIONS" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <!-- widget --> <receiver android:name=".widgets.EDriveWidget" Loading @@ -62,21 +77,21 @@ <!-- eDrive --> <provider android:name=".providers.MediasSyncProvider" android:authorities="foundation.e.drive.providers.MediasSyncProvider" android:authorities="${applicationId}.providers.MediasSyncProvider" android:enabled="true" android:exported="true" android:label="@string/account_setting_media_sync" tools:ignore="ExportedContentProvider" /> <provider android:name=".providers.SettingsSyncProvider" android:authorities="foundation.e.drive.providers.SettingsSyncProvider" android:authorities="${applicationId}.providers.SettingsSyncProvider" android:enabled="true" android:exported="true" android:label="@string/account_setting_app_sync" tools:ignore="ExportedContentProvider" /> <provider android:name=".providers.MeteredConnectionAllowedProvider" android:authorities="foundation.e.drive.providers.MeteredConnectionAllowedProvider" android:authorities="${applicationId}.providers.MeteredConnectionAllowedProvider" android:enabled="true" android:exported="true" android:label="@string/account_setting_metered_network" Loading Loading @@ -107,7 +122,7 @@ android:name=".account.receivers.AccountRemoveCallbackReceiver" android:exported="true" android:enabled="true" android:permission="foundation.e.accountmanager.permission.ACCOUNT_EVENTS"> android:permission="foundation.e.accountmanager.ng.permission.ACCOUNT_EVENTS"> <intent-filter> <action android:name="foundation.e.accountmanager.action.ACCOUNT_REMOVED"/> </intent-filter> Loading @@ -116,12 +131,34 @@ <receiver android:name=".account.receivers.AccountAddedReceiver" android:exported="true" android:permission="foundation.e.accountmanager.permission.ACCOUNT_EVENTS"> android:permission="foundation.e.accountmanager.ng.permission.ACCOUNT_EVENTS"> <intent-filter> <action android:name="foundation.e.accountmanager.action.MURENA_ACCOUNT_ADDED"/> </intent-filter> </receiver> <receiver android:name=".account.receivers.AppPasswordChangedReceiver" android:exported="true" android:permission="foundation.e.accountmanager.ng.permission.ACCOUNT_EVENTS"> <intent-filter> <action android:name="foundation.e.accountmanager.action.ACCOUNT_ADDED"/> <action android:name="foundation.e.accountmanager.action.APP_PASSWORD_CHANGED"/> </intent-filter> </receiver> <service android:name=".sync.MediaSyncService" android:exported="true" android:label="@string/account_setting_media_sync" android:permission="android.permission.BIND_SYNC_ADAPTER"> <intent-filter> <action android:name="android.content.SyncAdapter" /> </intent-filter> <meta-data android:name="android.content.SyncAdapter" android:resource="@xml/media_sync_adapter" /> </service> <service android:name="androidx.work.impl.foreground.SystemForegroundService" android:foregroundServiceType="dataSync" tools:node="merge" /> Loading app/src/main/java/foundation/e/drive/account/AccountUserInfoWorker.java +18 −10 Original line number Diff line number Diff line Loading @@ -56,6 +56,7 @@ public class AccountUserInfoWorker extends Worker { public static final String UNIQUE_WORK_NAME = "AccountUserInfoWorker"; private final AccountManager accountManager; private final MurenaAccountUserData userData; private final Context context; private Account account; Loading @@ -63,6 +64,7 @@ public class AccountUserInfoWorker extends Worker { super(context, workerParams); this.context = context; accountManager = AccountManager.get(context); userData = MurenaAccountUserData.from(context); account = CommonUtils.getAccount(context.getString(R.string.eelo_account_type), accountManager); } Loading Loading @@ -113,12 +115,12 @@ public class AccountUserInfoWorker extends Worker { if (ocsResult.isSuccess()) { final UserInfo userInfo = ocsResult.getResultData(); if (accountManager.getUserData(account, ACCOUNT_USER_ID_KEY) == null) { if (userData.get(account, ACCOUNT_USER_ID_KEY) == null) { final String userId = userInfo.getId(); if (userId != null) { client.setUserId(userId); AccountManager.get(context).setUserData(account, ACCOUNT_USER_ID_KEY, userId); userData.set(account, ACCOUNT_USER_ID_KEY, userId); Timber.v("UserId %s saved for account", userId); } } Loading @@ -127,13 +129,19 @@ public class AccountUserInfoWorker extends Worker { final String groups = (userInfo.getGroups() != null) ? String.join(",", userInfo.getGroups()) : ""; accountManager.setUserData(account, ACCOUNT_DATA_NAME, userInfo.getDisplayName()); accountManager.setUserData(account, ACCOUNT_DATA_EMAIL, userInfo.getEmail()); accountManager.setUserData(account, ACCOUNT_DATA_GROUPS, groups); userData.set(account, ACCOUNT_DATA_NAME, userInfo.getDisplayName()); userData.set(account, ACCOUNT_DATA_EMAIL, userInfo.getEmail()); userData.set(account, ACCOUNT_DATA_GROUPS, groups); Timber.d("fetchUserInfo(): success"); return true; } if (ocsResult.getCode() == RemoteOperationResult.ResultCode.UNAUTHORIZED) { Timber.w("fetchUserInfo(): unauthorized, refreshing app password"); DavClientProvider.getInstance().refreshCredentials(account, context); } Timber.d("fetchUserInfo(): failure"); return false; } Loading @@ -154,9 +162,9 @@ public class AccountUserInfoWorker extends Worker { relativeQuota = userQuota.getRelative(); } accountManager.setUserData(account, ACCOUNT_DATA_TOTAL_QUOTA_KEY, "" + totalQuota); accountManager.setUserData(account, ACCOUNT_DATA_RELATIVE_QUOTA_KEY, "" + relativeQuota); accountManager.setUserData(account, ACCOUNT_DATA_USED_QUOTA_KEY, "" + usedQuota); userData.set(account, ACCOUNT_DATA_TOTAL_QUOTA_KEY, "" + totalQuota); userData.set(account, ACCOUNT_DATA_RELATIVE_QUOTA_KEY, "" + relativeQuota); userData.set(account, ACCOUNT_DATA_USED_QUOTA_KEY, "" + usedQuota); addNotifAboutQuota(relativeQuota); } Loading Loading @@ -229,7 +237,7 @@ public class AccountUserInfoWorker extends Worker { } private boolean fetchAliases(NextcloudClient client) { final String userId = accountManager.getUserData(account, ACCOUNT_USER_ID_KEY); final String userId = userData.get(account, ACCOUNT_USER_ID_KEY); if (userId == null || userId.isEmpty()) { return false; Loading @@ -245,7 +253,7 @@ public class AccountUserInfoWorker extends Worker { aliases = String.join(",", aliasList); } } accountManager.setUserData(account, ACCOUNT_DATA_ALIAS_KEY, aliases); userData.set(account, ACCOUNT_DATA_ALIAS_KEY, aliases); Timber.d("fetchAliases(): success: %s", ocsResult.isSuccess()); DavClientProvider.getInstance().saveAccounts(context); Loading app/src/main/java/foundation/e/drive/account/AccountUtils.kt +2 −2 Original line number Diff line number Diff line Loading @@ -26,9 +26,9 @@ import foundation.e.drive.utils.AppConstants.SHARED_PREFERENCE_NAME object AccountUtils { @JvmStatic fun getPremiumPlan(accountManager: AccountManager, account: Account?): String? { fun getPremiumPlan(context: Context, account: Account?): String? { if (account == null) return null val groupData = accountManager.getUserData(account, ACCOUNT_DATA_GROUPS) val groupData = MurenaAccountUserData.from(context).get(account, ACCOUNT_DATA_GROUPS) val premiumGroup = extractPremiumGroup(groupData) return extractPremiumPlan(premiumGroup) } Loading app/src/main/java/foundation/e/drive/account/DrivePermissionRequestActivity.kt 0 → 100644 +114 −0 Original line number Diff line number Diff line /* * Copyright (C) MURENA SAS 2026 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.drive.account import android.Manifest import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import foundation.e.drive.utils.AppConstants import foundation.e.drive.work.WorkLauncher import timber.log.Timber /** * Foreground entry point launched by AccountManagerNG right after a Murena account is added * (action [ACTION_REQUEST_DRIVE_PERMISSIONS]). It bootstraps the two permissions eDrive can't * grant silently: POST_NOTIFICATIONS (so sync/permission notifications can be shown) and * All-Files-Access (so media can be read for upload). A background receiver can do neither, which * is why this has to be an Activity. */ class DrivePermissionRequestActivity : ComponentActivity() { private val notificationPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission(), ) { Timber.d("POST_NOTIFICATIONS result: %s", it) continueToStorageAccess() } private val allFilesAccessLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult(), ) { finishUp() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (needsNotificationPermission()) { notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) } else { continueToStorageAccess() } } private fun needsNotificationPermission(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED private fun continueToStorageAccess() { if (StoragePermissionNotifier.hasAllFilesAccess()) { finishUp() return } val intent = Intent( Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:$packageName"), ) runCatching { allFilesAccessLauncher.launch(intent) } .onFailure { Timber.w(it, "Could not open All-Files-Access settings") finishUp() } } private fun finishUp() { if (StoragePermissionNotifier.hasAllFilesAccess()) { StoragePermissionNotifier.cancel(this) resumeSynchronization() } finish() } private fun resumeSynchronization() { val workLauncher = WorkLauncher.getInstance(this) if (isSetupComplete()) { workLauncher.enqueueOneTimeFullScan(true) } else { workLauncher.enqueueSetupWorkers(this) } } private fun isSetupComplete(): Boolean = getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE) .getBoolean(AppConstants.SETUP_COMPLETED, false) companion object { const val ACTION_REQUEST_DRIVE_PERMISSIONS = "foundation.e.accountmanager.action.REQUEST_DRIVE_PERMISSIONS" } } Loading
app/build.gradle +14 −4 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ plugins { def versionMajor = 1 def versionMinor = 9 def versionPatch = 2 def versionPatch = 3 def getTestProp(String propName) { def result = "" Loading Loading @@ -39,7 +39,7 @@ android { compileSdk = 36 defaultConfig { applicationId = "foundation.e.drive" applicationId = "foundation.e.drive.ng" minSdk = 26 targetSdk = 36 versionCode = versionMajor * 1000000 + versionMinor * 1000 + versionPatch Loading @@ -49,6 +49,8 @@ android { manifestPlaceholders = [ 'appAuthRedirectScheme': applicationId, ] resValue "string", "media_sync_provider_authority", "${applicationId}.providers.MediasSyncProvider" } splits { Loading @@ -67,18 +69,26 @@ android { keyAlias = 'platform' keyPassword = 'android' } if (rootProject.file("../murena-test.jks").exists()) { murenaTest { storeFile = rootProject.file("../murena-test.jks") storePassword = 'murena' keyAlias = 'murena' keyPassword = 'murena' } } } buildTypes { release { minifyEnabled = false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig = signingConfigs.debugConfig signingConfig = signingConfigs.findByName('murenaTest') ?: signingConfigs.debugConfig buildConfigField("String", "SENTRY_DSN", "\"${getSentryDsn()}\"") } debug { signingConfig = signingConfigs.debugConfig signingConfig = signingConfigs.findByName('murenaTest') ?: signingConfigs.debugConfig buildConfigField("String", "SENTRY_DSN", "\"dummy\"") } } Loading
app/src/main/AndroidManifest.xml +44 −7 Original line number Diff line number Diff line Loading @@ -27,7 +27,11 @@ tools:ignore="QueryAllPackagesPermission" /> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="foundation.e.accountmanager.permission.ACCOUNT_EVENTS"/> <uses-permission android:name="foundation.e.accountmanager.ng.permission.ACCOUNT_EVENTS"/> <queries> <package android:name="foundation.e.accountmanager.ng" /> </queries> <application android:name=".EdriveApplication" Loading @@ -43,6 +47,17 @@ android:label="@string/my_account" android:theme="@style/AccountActivityTheme" /> <activity android:name=".account.DrivePermissionRequestActivity" android:exported="true" android:permission="foundation.e.accountmanager.ng.permission.ACCOUNT_EVENTS" android:theme="@android:style/Theme.Translucent.NoTitleBar"> <intent-filter> <action android:name="foundation.e.accountmanager.action.REQUEST_DRIVE_PERMISSIONS" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <!-- widget --> <receiver android:name=".widgets.EDriveWidget" Loading @@ -62,21 +77,21 @@ <!-- eDrive --> <provider android:name=".providers.MediasSyncProvider" android:authorities="foundation.e.drive.providers.MediasSyncProvider" android:authorities="${applicationId}.providers.MediasSyncProvider" android:enabled="true" android:exported="true" android:label="@string/account_setting_media_sync" tools:ignore="ExportedContentProvider" /> <provider android:name=".providers.SettingsSyncProvider" android:authorities="foundation.e.drive.providers.SettingsSyncProvider" android:authorities="${applicationId}.providers.SettingsSyncProvider" android:enabled="true" android:exported="true" android:label="@string/account_setting_app_sync" tools:ignore="ExportedContentProvider" /> <provider android:name=".providers.MeteredConnectionAllowedProvider" android:authorities="foundation.e.drive.providers.MeteredConnectionAllowedProvider" android:authorities="${applicationId}.providers.MeteredConnectionAllowedProvider" android:enabled="true" android:exported="true" android:label="@string/account_setting_metered_network" Loading Loading @@ -107,7 +122,7 @@ android:name=".account.receivers.AccountRemoveCallbackReceiver" android:exported="true" android:enabled="true" android:permission="foundation.e.accountmanager.permission.ACCOUNT_EVENTS"> android:permission="foundation.e.accountmanager.ng.permission.ACCOUNT_EVENTS"> <intent-filter> <action android:name="foundation.e.accountmanager.action.ACCOUNT_REMOVED"/> </intent-filter> Loading @@ -116,12 +131,34 @@ <receiver android:name=".account.receivers.AccountAddedReceiver" android:exported="true" android:permission="foundation.e.accountmanager.permission.ACCOUNT_EVENTS"> android:permission="foundation.e.accountmanager.ng.permission.ACCOUNT_EVENTS"> <intent-filter> <action android:name="foundation.e.accountmanager.action.MURENA_ACCOUNT_ADDED"/> </intent-filter> </receiver> <receiver android:name=".account.receivers.AppPasswordChangedReceiver" android:exported="true" android:permission="foundation.e.accountmanager.ng.permission.ACCOUNT_EVENTS"> <intent-filter> <action android:name="foundation.e.accountmanager.action.ACCOUNT_ADDED"/> <action android:name="foundation.e.accountmanager.action.APP_PASSWORD_CHANGED"/> </intent-filter> </receiver> <service android:name=".sync.MediaSyncService" android:exported="true" android:label="@string/account_setting_media_sync" android:permission="android.permission.BIND_SYNC_ADAPTER"> <intent-filter> <action android:name="android.content.SyncAdapter" /> </intent-filter> <meta-data android:name="android.content.SyncAdapter" android:resource="@xml/media_sync_adapter" /> </service> <service android:name="androidx.work.impl.foreground.SystemForegroundService" android:foregroundServiceType="dataSync" tools:node="merge" /> Loading
app/src/main/java/foundation/e/drive/account/AccountUserInfoWorker.java +18 −10 Original line number Diff line number Diff line Loading @@ -56,6 +56,7 @@ public class AccountUserInfoWorker extends Worker { public static final String UNIQUE_WORK_NAME = "AccountUserInfoWorker"; private final AccountManager accountManager; private final MurenaAccountUserData userData; private final Context context; private Account account; Loading @@ -63,6 +64,7 @@ public class AccountUserInfoWorker extends Worker { super(context, workerParams); this.context = context; accountManager = AccountManager.get(context); userData = MurenaAccountUserData.from(context); account = CommonUtils.getAccount(context.getString(R.string.eelo_account_type), accountManager); } Loading Loading @@ -113,12 +115,12 @@ public class AccountUserInfoWorker extends Worker { if (ocsResult.isSuccess()) { final UserInfo userInfo = ocsResult.getResultData(); if (accountManager.getUserData(account, ACCOUNT_USER_ID_KEY) == null) { if (userData.get(account, ACCOUNT_USER_ID_KEY) == null) { final String userId = userInfo.getId(); if (userId != null) { client.setUserId(userId); AccountManager.get(context).setUserData(account, ACCOUNT_USER_ID_KEY, userId); userData.set(account, ACCOUNT_USER_ID_KEY, userId); Timber.v("UserId %s saved for account", userId); } } Loading @@ -127,13 +129,19 @@ public class AccountUserInfoWorker extends Worker { final String groups = (userInfo.getGroups() != null) ? String.join(",", userInfo.getGroups()) : ""; accountManager.setUserData(account, ACCOUNT_DATA_NAME, userInfo.getDisplayName()); accountManager.setUserData(account, ACCOUNT_DATA_EMAIL, userInfo.getEmail()); accountManager.setUserData(account, ACCOUNT_DATA_GROUPS, groups); userData.set(account, ACCOUNT_DATA_NAME, userInfo.getDisplayName()); userData.set(account, ACCOUNT_DATA_EMAIL, userInfo.getEmail()); userData.set(account, ACCOUNT_DATA_GROUPS, groups); Timber.d("fetchUserInfo(): success"); return true; } if (ocsResult.getCode() == RemoteOperationResult.ResultCode.UNAUTHORIZED) { Timber.w("fetchUserInfo(): unauthorized, refreshing app password"); DavClientProvider.getInstance().refreshCredentials(account, context); } Timber.d("fetchUserInfo(): failure"); return false; } Loading @@ -154,9 +162,9 @@ public class AccountUserInfoWorker extends Worker { relativeQuota = userQuota.getRelative(); } accountManager.setUserData(account, ACCOUNT_DATA_TOTAL_QUOTA_KEY, "" + totalQuota); accountManager.setUserData(account, ACCOUNT_DATA_RELATIVE_QUOTA_KEY, "" + relativeQuota); accountManager.setUserData(account, ACCOUNT_DATA_USED_QUOTA_KEY, "" + usedQuota); userData.set(account, ACCOUNT_DATA_TOTAL_QUOTA_KEY, "" + totalQuota); userData.set(account, ACCOUNT_DATA_RELATIVE_QUOTA_KEY, "" + relativeQuota); userData.set(account, ACCOUNT_DATA_USED_QUOTA_KEY, "" + usedQuota); addNotifAboutQuota(relativeQuota); } Loading Loading @@ -229,7 +237,7 @@ public class AccountUserInfoWorker extends Worker { } private boolean fetchAliases(NextcloudClient client) { final String userId = accountManager.getUserData(account, ACCOUNT_USER_ID_KEY); final String userId = userData.get(account, ACCOUNT_USER_ID_KEY); if (userId == null || userId.isEmpty()) { return false; Loading @@ -245,7 +253,7 @@ public class AccountUserInfoWorker extends Worker { aliases = String.join(",", aliasList); } } accountManager.setUserData(account, ACCOUNT_DATA_ALIAS_KEY, aliases); userData.set(account, ACCOUNT_DATA_ALIAS_KEY, aliases); Timber.d("fetchAliases(): success: %s", ocsResult.isSuccess()); DavClientProvider.getInstance().saveAccounts(context); Loading
app/src/main/java/foundation/e/drive/account/AccountUtils.kt +2 −2 Original line number Diff line number Diff line Loading @@ -26,9 +26,9 @@ import foundation.e.drive.utils.AppConstants.SHARED_PREFERENCE_NAME object AccountUtils { @JvmStatic fun getPremiumPlan(accountManager: AccountManager, account: Account?): String? { fun getPremiumPlan(context: Context, account: Account?): String? { if (account == null) return null val groupData = accountManager.getUserData(account, ACCOUNT_DATA_GROUPS) val groupData = MurenaAccountUserData.from(context).get(account, ACCOUNT_DATA_GROUPS) val premiumGroup = extractPremiumGroup(groupData) return extractPremiumPlan(premiumGroup) } Loading
app/src/main/java/foundation/e/drive/account/DrivePermissionRequestActivity.kt 0 → 100644 +114 −0 Original line number Diff line number Diff line /* * Copyright (C) MURENA SAS 2026 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.drive.account import android.Manifest import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import foundation.e.drive.utils.AppConstants import foundation.e.drive.work.WorkLauncher import timber.log.Timber /** * Foreground entry point launched by AccountManagerNG right after a Murena account is added * (action [ACTION_REQUEST_DRIVE_PERMISSIONS]). It bootstraps the two permissions eDrive can't * grant silently: POST_NOTIFICATIONS (so sync/permission notifications can be shown) and * All-Files-Access (so media can be read for upload). A background receiver can do neither, which * is why this has to be an Activity. */ class DrivePermissionRequestActivity : ComponentActivity() { private val notificationPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission(), ) { Timber.d("POST_NOTIFICATIONS result: %s", it) continueToStorageAccess() } private val allFilesAccessLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult(), ) { finishUp() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (needsNotificationPermission()) { notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) } else { continueToStorageAccess() } } private fun needsNotificationPermission(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED private fun continueToStorageAccess() { if (StoragePermissionNotifier.hasAllFilesAccess()) { finishUp() return } val intent = Intent( Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:$packageName"), ) runCatching { allFilesAccessLauncher.launch(intent) } .onFailure { Timber.w(it, "Could not open All-Files-Access settings") finishUp() } } private fun finishUp() { if (StoragePermissionNotifier.hasAllFilesAccess()) { StoragePermissionNotifier.cancel(this) resumeSynchronization() } finish() } private fun resumeSynchronization() { val workLauncher = WorkLauncher.getInstance(this) if (isSetupComplete()) { workLauncher.enqueueOneTimeFullScan(true) } else { workLauncher.enqueueSetupWorkers(this) } } private fun isSetupComplete(): Boolean = getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE) .getBoolean(AppConstants.SETUP_COMPLETED, false) companion object { const val ACTION_REQUEST_DRIVE_PERMISSIONS = "foundation.e.accountmanager.action.REQUEST_DRIVE_PERMISSIONS" } }