Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request account visibility, implements #728 #736

Merged
merged 1 commit into from
Jan 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Request account visibility, implements #728
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.
  • Loading branch information
dmfs committed Jan 11, 2019
commit bb1546af914d7a5c48523e22885fc0d59e053764
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
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;
Expand Down Expand Up @@ -76,8 +77,10 @@
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;


/**
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -911,7 +927,15 @@ public Uri insertInTransaction(final SQLiteDatabase db, Uri uri, final ContentVa

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:
Expand Down Expand Up @@ -1302,6 +1326,13 @@ protected void onEndTransaction(boolean callerIsSyncAdapter)
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);
}
}


Expand Down Expand Up @@ -1346,6 +1377,8 @@ protected boolean syncToNetwork(Uri uri)
@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);
}
Expand Down
1 change: 1 addition & 0 deletions opentasks/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down
11 changes: 10 additions & 1 deletion opentasks/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

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

Expand Down Expand Up @@ -87,7 +88,8 @@ protected void onStop()

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");
}
Expand Down
2 changes: 1 addition & 1 deletion opentaskspal/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ android {

dependencies {
implementation project(':opentasks-contract')
implementation(deps.contentpal) {
api(deps.contentpal) {
exclude module: 'jems'
}
implementation deps.datetime
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.opentaskspal.predicates;

import android.support.annotation.NonNull;

import org.dmfs.android.contentpal.Predicate;
import org.dmfs.android.contentpal.predicates.DelegatingPredicate;


/**
* A {@link Predicate} which evaluates to {@code true}, if and only if at least one of the given {@link Predicate}s evaluates to {@code true}. This corresponds
* to the Boolean "or" operation.
*
* @author Marten Gajda
* @deprecated This should be updated in ContentPal, see https://github.com/dmfs/ContentPal/issues/173
*/
public final class AnyOf extends DelegatingPredicate
{
public AnyOf(@NonNull Predicate... predicates)
{
super(new BinaryPredicate("or", predicates));
}


public AnyOf(@NonNull Iterable<Predicate> predicates)
{
super(new BinaryPredicate("or", predicates));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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.opentaskspal.predicates;

import android.support.annotation.NonNull;

import org.dmfs.android.contentpal.Predicate;
import org.dmfs.android.contentpal.TransactionContext;
import org.dmfs.iterables.elementary.Seq;
import org.dmfs.jems.iterable.composite.Joined;
import org.dmfs.jems.iterable.decorators.Mapped;

import java.util.Iterator;


/**
* A {@link Predicate} which connects a number of given predicates with the given binary operator. Like this:
* <pre>{@code
* (predicate_1) operator (predicate_2) operator (predicate_3) … operator (predicate_n)
* }</pre>
* <p>
* If no predicates are given this always evaluates to "1".
*
* @author Marten Gajda
* @deprecated This should be updated in ContentPal, see https://github.com/dmfs/ContentPal/issues/173
*/
@Deprecated
public final class BinaryPredicate implements Predicate
{
private final Iterable<Predicate> mPredicates;
private final String mOperator;


public BinaryPredicate(@NonNull String operator, @NonNull Predicate... predicates)
{
this(operator, new Seq<>(predicates));
}


public BinaryPredicate(@NonNull String operator, @NonNull Iterable<Predicate> predicates)
{
mOperator = operator;
mPredicates = predicates;
}


@NonNull
@Override
public CharSequence selection(@NonNull TransactionContext transactionContext)
{
Iterator<Predicate> iterator = mPredicates.iterator();
if (!iterator.hasNext())
{
return "1";
}
StringBuilder result = new StringBuilder(256);
result.append("( ");
result.append(iterator.next().selection(transactionContext));
while (iterator.hasNext())
{
result.append(" ) ");
result.append(mOperator);
result.append(" ( ");
result.append(iterator.next().selection(transactionContext));
}
result.append(" )");
return result;

}


@NonNull
@Override
public Iterable<Argument> arguments(@NonNull final TransactionContext transactionContext)
{
return new Joined<>(
new Mapped<>(
predicate -> predicate.arguments(transactionContext),
mPredicates));
}
}
Loading