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

Unverified Commit 5b18f803 authored by Marten Gajda's avatar Marten Gajda Committed by GitHub
Browse files

Request account visibility, implements #728 (#736)

This commit adds a workaround for sync apps which don't grant account visibility to OpenTasks. If a task list is inserted which belongs to an account which we can't see, the app asks for permission to see the account.
The `GET_ACCOUNTS` permission is no longer used on Android 8+ and the request for that permission will no longer be shown when the app starts.
parent fc9976e3
Loading
Loading
Loading
Loading
+34 −1
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.text.TextUtils;
import android.util.Log;

import org.dmfs.iterables.EmptyIterable;
import org.dmfs.provider.tasks.TaskDatabaseHelper.OnDatabaseOperationListener;
@@ -76,8 +77,10 @@ import org.dmfs.tasks.contract.TaskContract.TaskLists;
import org.dmfs.tasks.contract.TaskContract.Tasks;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;


/**
@@ -116,6 +119,7 @@ public final class TaskProvider extends SQLiteContentProvider implements OnAccou
    private static final int OPERATIONS = 100000;

    private final static Set<String> TASK_LIST_SYNC_COLUMNS = new HashSet<String>(Arrays.asList(TaskLists.SYNC_ADAPTER_COLUMNS));
    private static final String TAG = "TaskProvider";

    /**
     * A list of {@link EntityProcessor}s to execute when doing operations on the instances table.
@@ -152,6 +156,18 @@ public final class TaskProvider extends SQLiteContentProvider implements OnAccou
     */
    private ProviderOperationsLog mOperationsLog = new ProviderOperationsLog();

    /**
     * This is a per transaction/thread flag which indicates whether new lists with an unknown account have been added.
     * If this holds true at the end of a transaction a window should be shown to ask the user for access to that account.
     */
    private ThreadLocal<Boolean> mStaleListCreated = new ThreadLocal<>();

    /**
     * The currently known accounts. This may be accessed from various threads, hence the AtomicReference.
     * By statring with an empty set, we can always guarantee a non-null reference.
     */
    private AtomicReference<Set<Account>> mAccountCache = new AtomicReference<>(Collections.emptySet());


    public TaskProvider()
    {
@@ -911,7 +927,15 @@ public final class TaskProvider extends SQLiteContentProvider implements OnAccou

                rowId = list.id();
                result_uri = TaskContract.TaskLists.getContentUri(mAuthority);

                // if the account is unknown we need to ask the user
                if (Build.VERSION.SDK_INT >= 26 &&
                        !TaskContract.LOCAL_ACCOUNT_TYPE.equals(accountType) &&
                        !mAccountCache.get().contains(new Account(accountName, accountType)))
                {
                    // store the fact that we have an unknown account in this transaction
                    mStaleListCreated.set(true);
                    Log.d(TAG, String.format("List with unknown account %s inserted.", new Account(accountName, accountType)));
                }
                break;
            }
            case TASKS:
@@ -1302,6 +1326,13 @@ public final class TaskProvider extends SQLiteContentProvider implements OnAccou
            providerChangedIntent.setPackage(getContext().getPackageName());
        }
        getContext().sendBroadcast(providerChangedIntent);

        if (Boolean.TRUE.equals(mStaleListCreated.get()))
        {
            // notify UI about the stale lists, it's up the UI to deal with this, either by showing a notification or an instant popup.
            Intent visbilityRequest = new Intent("org.dmfs.tasks.action.STALE_LIST_BROADCAST").setPackage(getContext().getPackageName());
            getContext().sendBroadcast(visbilityRequest);
        }
    }


@@ -1346,6 +1377,8 @@ public final class TaskProvider extends SQLiteContentProvider implements OnAccou
    @Override
    public void onAccountsUpdated(Account[] accounts)
    {
        // cache the known accounts so we can check whether we know accounts for which new lists are added
        mAccountCache.set(new HashSet<>(Arrays.asList(accounts)));
        // TODO: we probably can move the cleanup code here and get rid of the Utils class
        Utils.cleanUpLists(getContext(), getDatabaseHelper().getWritableDatabase(), accounts, mAuthority);
    }
+1 −0
Original line number Diff line number Diff line
@@ -90,6 +90,7 @@ dependencies {
    androidTestImplementation(deps.support_test_rules) {
        exclude group: 'com.android.support', module: 'support-annotations'
    }
    compile project(path: ':opentaskspal')
}

if (project.hasProperty('PLAY_STORE_SERVICE_ACCOUNT_CREDENTIALS')) {
+10 −1
Original line number Diff line number Diff line
@@ -5,7 +5,9 @@

    <uses-permission android:name="org.dmfs.permission.READ_TASKS"/>
    <uses-permission android:name="org.dmfs.permission.WRITE_TASKS"/>
    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
    <uses-permission
            android:name="android.permission.GET_ACCOUNTS"
            android:maxSdkVersion="25"/>
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
    <uses-permission android:name="android.permission.VIBRATE"/>
@@ -347,6 +349,13 @@
                        android:scheme="content"/>
            </intent-filter>
        </receiver>
        <receiver
                android:name=".StaleListBroadcastReceiver"
                android:exported="false">
            <intent-filter>
                <action android:name="org.dmfs.tasks.action.STALE_LIST_BROADCAST"/>
            </intent-filter>
        </receiver>

        <service
                android:name="org.dmfs.tasks.notification.NotificationUpdaterService"
+90 −0
Original line number Diff line number Diff line
/*
 * Copyright 2018 dmfs GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.dmfs.tasks;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;

import org.dmfs.android.contentpal.RowDataSnapshot;
import org.dmfs.android.contentpal.RowSnapshot;
import org.dmfs.android.contentpal.predicates.AccountEq;
import org.dmfs.android.contentpal.predicates.EqArg;
import org.dmfs.android.contentpal.predicates.Not;
import org.dmfs.android.contentpal.projections.MultiProjection;
import org.dmfs.android.contentpal.rowsets.QueryRowSet;
import org.dmfs.iterables.elementary.Seq;
import org.dmfs.jems.iterable.composite.Joined;
import org.dmfs.jems.iterable.decorators.Mapped;
import org.dmfs.opentaskspal.predicates.AnyOf;
import org.dmfs.opentaskspal.views.TaskListsView;
import org.dmfs.provider.tasks.AuthorityUtil;
import org.dmfs.tasks.contract.TaskContract;

import java.util.ArrayList;

import static java.util.Collections.singletonList;


/**
 * @author Marten Gajda
 */
public final class StaleListBroadcastReceiver extends BroadcastReceiver
{
    @Override
    public void onReceive(Context context, Intent intent)
    {
        if (Build.VERSION.SDK_INT < 26)
        {
            // this receiver is Android 8+ only
            return;
        }
        AccountManager accountManager = AccountManager.get(context);
        String authority = AuthorityUtil.taskAuthority(context);
        String description = String.format("Please give %s access to the following account", context.getString(R.string.app_name));
        // request access to each account we don't know yet individually
        for (Intent accountRequestIntent :
                new Mapped<>(
                        account -> AccountManager.newChooseAccountIntent(account, new ArrayList<Account>(singletonList(account)), null,
                                description, null,
                                null, null),
                        new Mapped<RowDataSnapshot<TaskContract.TaskLists>, Account>(
                                this::account,
                                new Mapped<>(RowSnapshot::values,
                                        new QueryRowSet<>(
                                                new TaskListsView(authority, context.getContentResolver().acquireContentProviderClient(authority)),
                                                new MultiProjection<>(TaskContract.TaskLists.ACCOUNT_NAME, TaskContract.TaskLists.ACCOUNT_TYPE),
                                                new Not(new AnyOf(
                                                        new Joined<>(new Seq<>(
                                                                new EqArg(TaskContract.TaskLists.ACCOUNT_TYPE, TaskContract.LOCAL_ACCOUNT_TYPE)),
                                                                new Mapped<>(AccountEq::new, new Seq<>(accountManager.getAccounts()))))))))))
        {
            context.startActivity(accountRequestIntent);
        }
    }


    private Account account(RowDataSnapshot<TaskContract.TaskLists> data)
    {
        return (new Account(
                data.data(TaskContract.TaskLists.ACCOUNT_NAME, s -> s).value(),
                data.data(TaskContract.TaskLists.ACCOUNT_TYPE, s -> s).value()));
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package org.dmfs.tasks.utils;

import android.Manifest;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

@@ -87,7 +88,8 @@ public abstract class BaseActivity extends AppCompatActivity

    private void requestMissingGetAccountsPermission()
    {
        if (!mGetAccountsPermission.isGranted())
        /* This is only a thing on Android SDK Level <26. The permission has been replaced with per-account visibility. */
        if (Build.VERSION.SDK_INT < 26 && !mGetAccountsPermission.isGranted())
        {
            PermissionRequestDialogFragment.newInstance(mGetAccountsPermission.isRequestable(this)).show(getSupportFragmentManager(), "permission-dialog");
        }
Loading