Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5781966b authored by Jonathan Klee's avatar Jonathan Klee
Browse files

feat(ng): integrate eDrive with AccountManagerNG

parent 5f082230
Loading
Loading
Loading
Loading
Loading
+14 −4
Original line number Diff line number Diff line
@@ -5,7 +5,7 @@ plugins {

def versionMajor = 1
def versionMinor = 9
def versionPatch = 2
def versionPatch = 3

def getTestProp(String propName) {
    def result = ""
@@ -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
@@ -49,6 +49,8 @@ android {
        manifestPlaceholders = [
                'appAuthRedirectScheme':  applicationId,
        ]

        resValue "string", "media_sync_provider_authority", "${applicationId}.providers.MediasSyncProvider"
    }

    splits {
@@ -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\"")
        }
    }
+44 −7
Original line number Diff line number Diff line
@@ -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"
@@ -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"
@@ -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"
@@ -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>
@@ -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" />
+18 −10
Original line number Diff line number Diff line
@@ -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;

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

@@ -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);
                }
            }
@@ -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;
    }
@@ -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);
    }
@@ -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;
@@ -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);
+2 −2
Original line number Diff line number Diff line
@@ -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)
    }
+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