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

Commit cf1d198a authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

Merge branch '2985-edrive-murena-cloud-recovery' into 'main'

feat: Implement device to cloud push for all the files to resolve Murena.io recovery

See merge request !302
parents 9a290f19 231de74b
Loading
Loading
Loading
Loading
Loading
+23 −5
Original line number Diff line number Diff line
/*
 * Copyright © ECORP SAS 2022-2023.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 * Copyright (C) 2025 e Foundation
 * Copyright (C) ECORP SAS 2022-2023
 *
 * 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;
@@ -20,6 +30,7 @@ import androidx.annotation.NonNull;

import foundation.e.drive.database.FailedSyncPrefsManager;
import foundation.e.drive.fileObservers.FileObserverManager;
import foundation.e.drive.recovery.RecoveryManager;
import foundation.e.drive.utils.AppConstants;
import foundation.e.drive.utils.CommonUtils;
import foundation.e.drive.utils.ReleaseTree;
@@ -59,6 +70,13 @@ public class EdriveApplication extends Application {
        }

        FailedSyncPrefsManager.getInstance(getApplicationContext()).clearPreferences();

        setupEdriveRecovery();
    }

    private void setupEdriveRecovery() {
        RecoveryManager recoveryManager = new RecoveryManager(getApplicationContext());
        recoveryManager.initiateRecovery();
    }

    synchronized public void startRecursiveFileObserver() {
+104 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 e Foundation
 *
 * 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.accounts.AccountManager
import android.content.Context
import android.content.SharedPreferences
import foundation.e.drive.R
import foundation.e.drive.utils.AppConstants
import foundation.e.drive.utils.DavClientProvider
import foundation.e.drive.work.WorkLauncher
import timber.log.Timber

class AccountAdder(private val context: Context) {

    private val preferences: SharedPreferences by lazy {
        context.applicationContext.getSharedPreferences(
            AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE
        )
    }

    fun addAccount(name: String, type: String) {
        if (!canStart(name, type, preferences, context)) return

        updateAccountNameOnPreference(name)

        val workLauncher = WorkLauncher.getInstance(context)
        if (workLauncher.enqueueSetupWorkers(context)) {
            DavClientProvider.getInstance().cleanUp()
            workLauncher.enqueuePeriodicUserInfoFetching()
        }
    }

    private fun updateAccountNameOnPreference(name: String) {
        preferences.edit()
            .putString(AccountManager.KEY_ACCOUNT_NAME, name)
            .apply()
    }

    /**
     * Check that conditions to start are met:
     * - Setup has not already been done
     * - AccountName is not empty
     * - AccountType is /e/ account
     * - the account is effectively available through accountManager
     */
    private fun canStart(
        accountName: String,
        accountType: String,
        prefs: SharedPreferences,
        context: Context
    ): Boolean {
        if (isSetupAlreadyDone(prefs)) {
            Timber.w("Set up is already done, skipping account addition.")
            return false
        }

        if (accountName.isEmpty()) {
            Timber.w("Account name is empty, skipping account addition.")
            return false
        }

        if (isInvalidAccountType(accountType, context)) {
            Timber.w("Account type: $accountType is invalid, skipping account addition.")
            return false
        }

        if (!isExistingAccount(accountName, context)) {
            Timber.w("No account exists for $accountName, skipping account addition.")
            return false
        }

        return true
    }

    private fun isSetupAlreadyDone(prefs: SharedPreferences): Boolean {
        return prefs.getBoolean(AppConstants.SETUP_COMPLETED, false)
    }

    private fun isInvalidAccountType(accountType: String, context: Context): Boolean {
        val validAccountType = context.getString(R.string.eelo_account_type)
        return accountType != validAccountType
    }

    private fun isExistingAccount(accountName: String, context: Context): Boolean {
        return AccountUtils.getAccount(accountName, context) != null
    }
}
+116 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 e Foundation
 *
 * 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.accounts.AccountManager
import android.app.Application
import android.app.NotificationManager
import android.content.Context
import android.content.SharedPreferences
import androidx.work.WorkManager
import foundation.e.drive.database.DbHelper
import foundation.e.drive.database.FailedSyncPrefsManager
import foundation.e.drive.synchronization.SyncProxy
import foundation.e.drive.utils.AppConstants
import foundation.e.drive.utils.AppConstants.INITIAL_FOLDER_NUMBER
import foundation.e.drive.utils.AppConstants.SETUP_COMPLETED
import foundation.e.drive.utils.DavClientProvider
import timber.log.Timber
import java.io.File

class AccountRemover(
    private val context: Context,
) {
    private val preferences: SharedPreferences by lazy {
        context.applicationContext.getSharedPreferences(
            AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE
        )
    }

    fun removeAccount() {
        cancelWorkers()
        setSyncStateToIdle()
        deleteDatabase()
        cleanSharedPreferences()
        removeCachedFiles()
        deleteNotificationChannels()
        cleanUpDavClient()
    }

    private fun cleanUpDavClient() {
        DavClientProvider.getInstance().cleanUp()
    }

    private fun setSyncStateToIdle() {
        SyncProxy.moveToIdle(context.applicationContext as Application)
    }

    private fun cancelWorkers() {
        val workManager = WorkManager.getInstance(context)
        workManager.cancelAllWorkByTag(AppConstants.WORK_GENERIC_TAG)
    }

    private fun deleteDatabase() {
        val result = context.applicationContext.deleteDatabase(DbHelper.DATABASE_NAME)
        Timber.d("Remove Database: %s", result)
    }

    private fun cleanSharedPreferences() {
        if (!context.applicationContext.deleteSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME)) {
            //If removal failed, clear all data inside
            preferences.edit().remove(AccountManager.KEY_ACCOUNT_NAME)
                .remove(AccountManager.KEY_ACCOUNT_TYPE)
                .remove(SETUP_COMPLETED)
                .remove(INITIAL_FOLDER_NUMBER)
                .remove(AppConstants.KEY_LAST_SCAN_TIME)
                .apply()
        }
        context.applicationContext.deleteSharedPreferences(FailedSyncPrefsManager.PREF_NAME)
    }

    private fun removeCachedFiles() {
        try {
            deleteDir(context.applicationContext.externalCacheDir)
        } catch (e: SecurityException) {
            Timber.e(e, "failed to delete cached file on account removal call")
        }
    }

    @Throws(SecurityException::class)
    private fun deleteDir(dir: File?): Boolean {
        if (dir == null) {
            Timber.w("cache file returned null. preventing a NPE")
            return false
        }

        return dir.deleteRecursively()
    }

    @Suppress("TooGenericExceptionCaught")
    private fun deleteNotificationChannels() {
        val notificationManager =
            context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        try {
            notificationManager.cancelAll()
        } catch (exception: RuntimeException) {
            Timber.e(exception, "Cannot cancel all notifications")
        }
    }
}
+19 −70
Original line number Diff line number Diff line
/*
 * Copyright © MURENA SAS 2023-2024.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 * Copyright (C) 2025 e Foundation
 * Copyright (C) MURENA SAS 2023-2024
 *
 * 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.receivers

@@ -11,12 +21,7 @@ import android.accounts.AccountManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import foundation.e.drive.R
import foundation.e.drive.account.AccountUtils
import foundation.e.drive.utils.AppConstants
import foundation.e.drive.utils.DavClientProvider
import foundation.e.drive.work.WorkLauncher
import foundation.e.drive.account.AccountAdder
import timber.log.Timber

/**
@@ -24,7 +29,7 @@ import timber.log.Timber
 * Triggered by AccountManager
 * @author Vincent Bourgmayer
 */
class AccountAddedReceiver() : BroadcastReceiver() {
class AccountAddedReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        Timber.d("\"Account added\" intent received")

@@ -33,66 +38,10 @@ class AccountAddedReceiver() : BroadcastReceiver() {
        val extras = intent.extras!!
        val accountName = extras.getString(AccountManager.KEY_ACCOUNT_NAME, "")
        val accountType = extras.getString(AccountManager.KEY_ACCOUNT_TYPE, "")
        val prefs = context.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME,
            Context.MODE_PRIVATE)

        Timber.d("AccountAddedReceiver.onReceive with name: %s and type: %s", accountName, accountType)

        if (!canStart(accountName, accountType, prefs, context)) return

        prefs.edit()
            .putString(AccountManager.KEY_ACCOUNT_NAME, accountName)
            .apply()

        val workLauncher = WorkLauncher.getInstance(context)
        if (workLauncher.enqueueSetupWorkers(context)) {
            DavClientProvider.getInstance().cleanUp()
            workLauncher.enqueuePeriodicUserInfoFetching()
        }
}

    /**
     * Check that conditions to start are met:
     * - Setup has not already been done
     * - AccountName is not empty
     * - AccountType is /e/ account
     * - the account is effectively available through accountManager
     */
    private fun canStart(
        accountName: String,
        accountType: String,
        prefs: SharedPreferences,
        context: Context
    ): Boolean {
        if (isSetupAlreadyDone(prefs)) {
            return false
        }

        if (accountName.isEmpty()) {
            return false
        }

        if (isInvalidAccountType(accountType, context)) {
            return false
        }

        if (!isExistingAccount(accountName, context)) {
            Timber.w("No account exist for username: %s ", accountType, accountName)
            return false
        }
        return true
    }

    private fun isInvalidAccountType(accountType: String, context: Context): Boolean {
        val validAccountType = context.getString(R.string.eelo_account_type)
        return accountType != validAccountType
    }

    private fun isSetupAlreadyDone(prefs: SharedPreferences): Boolean {
        return prefs.getBoolean(AppConstants.SETUP_COMPLETED, false)
    }

    private fun isExistingAccount(accountName: String, context: Context): Boolean {
        return AccountUtils.getAccount(accountName, context) != null
        val accountAdder = AccountAdder(context)
        accountAdder.addAccount(accountName, accountType)
    }
}
+21 −93
Original line number Diff line number Diff line
/*
 * Copyright © ECORP SAS 2023.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 * Copyright (C) 2025 e Foundation
 * Copyright (C) ECORP SAS 2023
 *
 * 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.receivers;

import static foundation.e.drive.utils.AppConstants.INITIAL_FOLDER_NUMBER;
import static foundation.e.drive.utils.AppConstants.SETUP_COMPLETED;

import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.work.WorkManager;

import java.io.File;

import foundation.e.drive.R;
import foundation.e.drive.database.DbHelper;
import foundation.e.drive.database.FailedSyncPrefsManager;
import foundation.e.drive.synchronization.SyncProxy;
import foundation.e.drive.account.AccountRemover;
import foundation.e.drive.utils.AppConstants;
import foundation.e.drive.utils.DavClientProvider;
import foundation.e.drive.utils.ViewUtils;
import timber.log.Timber;

public class AccountRemoveCallbackReceiver extends BroadcastReceiver {

    private static final String ACTION_ACCOUNT_REMOVED = "android.accounts.action.ACCOUNT_REMOVED";

    @SuppressLint("UnsafeProtectedBroadcastReceiver")
    @Override
    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
@@ -50,29 +50,12 @@ public class AccountRemoveCallbackReceiver extends BroadcastReceiver {
            return;
        }

        cancelWorkers(applicationContext);
        SyncProxy.INSTANCE.moveToIdle((Application) applicationContext);
        deleteDatabase(applicationContext);
        cleanSharedPreferences(applicationContext, preferences);
        removeCachedFiles(applicationContext);
        deleteNotificationChannels(applicationContext);

        DavClientProvider.getInstance().cleanUp();
        AccountRemover accountRemover = new AccountRemover(context);
        accountRemover.removeAccount();

        ViewUtils.updateWidgetView(applicationContext);
    }


    private void cancelWorkers(@NonNull Context context) {
        final WorkManager workManager = WorkManager.getInstance(context);
        workManager.cancelAllWorkByTag(AppConstants.WORK_GENERIC_TAG);
    }

    private void deleteDatabase(@NonNull Context applicationContext) {
        final boolean result = applicationContext.deleteDatabase(DbHelper.DATABASE_NAME);
        Timber.d("Remove Database: %s", result);
    }

    private boolean shouldProceedWithRemoval(@NonNull Intent intent, @NonNull SharedPreferences preferences, @NonNull Context context) {
        if (isInvalidAction(intent) || intent.getExtras() == null) {
            Timber.w("Invalid account removal request");
@@ -94,61 +77,6 @@ public class AccountRemoveCallbackReceiver extends BroadcastReceiver {
    }

    private boolean isInvalidAction(@NonNull Intent intent) {
        return !"android.accounts.action.ACCOUNT_REMOVED".equals(intent.getAction());
    }

    private void cleanSharedPreferences(@NonNull Context applicationContext, @NonNull SharedPreferences prefs) {
        if (!applicationContext.deleteSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME)) {
            //If removal failed, clear all data inside
            prefs.edit().remove(AccountManager.KEY_ACCOUNT_NAME)
                    .remove(AccountManager.KEY_ACCOUNT_TYPE)
                    .remove(SETUP_COMPLETED)
                    .remove(INITIAL_FOLDER_NUMBER)
                    .remove(AppConstants.KEY_LAST_SCAN_TIME)
                    .apply();
        }

        applicationContext.deleteSharedPreferences(FailedSyncPrefsManager.PREF_NAME);
    }

    private void removeCachedFiles(@NonNull Context applicationContext) {
        try {
            deleteDir(applicationContext.getExternalCacheDir());
        } catch (SecurityException e) {
            Timber.e(e, "failed to delete cached file on account removal call");
        }
    }

    private boolean deleteDir(@Nullable File dir) throws SecurityException {
        if (dir == null) {
            Timber.w("cache file returned null. preventing a NPE");
            return false;
        }

        if (dir.isDirectory()) {
            String[] children = dir.list();
            if (children == null) {
                return dir.delete();
            }

            for (String child : children) {
                boolean isSuccess = deleteDir(new File(dir, child));
                if (!isSuccess) {
                    Timber.w("Failed to remove the cached file: %s", child);
                }
            }
        }

        return dir.delete();
    }

    private void deleteNotificationChannels(Context context) {
        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        try {
            notificationManager.cancelAll();
        } catch (Exception exception) {
            Timber.e(exception, "Cannot cancel all notifications");
        }
        return !ACTION_ACCOUNT_REMOVED.equals(intent.getAction());
    }
}
Loading