Implement Android 6-style permissions

* increase target API level to 23 (Android 6), which makes Android 6-style permissions mandatory
* AUTHENTICATE_ACCOUNTS permission is only required up to API level 22
* new activity: PermissionsActivity which shows missing permissions and provides buttons to request them
* DavService: Android shouldn't send a null Intent, but sometimes it does, so implement null check
* LocalTaskList: tasksProviderAvailable may return true on API level 23+ even if permissions are not sufficient
* SyncAdapterService: show a notification (with Intent for PermissionsActivity) when permissions are not sufficient
* when creating accounts, set OpenTasks sync always to true if API level is 23+ (even if OpenTasks is not installed [yet])
* update Lombok
pull/2/head
Ricki Hirner 8 years ago
parent 59252d7471
commit 61231b4233

@ -15,7 +15,7 @@ android {
defaultConfig {
applicationId "at.bitfire.davdroid"
minSdkVersion 14
targetSdkVersion 22
targetSdkVersion 23
versionCode 96
versionName "1.0.6"
@ -60,7 +60,7 @@ configurations.all {
}
dependencies {
provided 'org.projectlombok:lombok:1.16.6'
provided 'org.projectlombok:lombok:1.16.8'
compile project(':dav4android')
compile project(':ical4android')

@ -21,6 +21,10 @@
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<!-- legacy permissions -->
<uses-permission
android:name="android.permission.AUTHENTICATE_ACCOUNTS"
android:maxSdkVersion="22"
tools:ignore="UnusedAttribute"/>
<!--
for writing external log files; permission only required for SDK <= 18 because since then,
writing to app-private directory doesn't require extra permissions
@ -35,7 +39,6 @@
tools:ignore="UnusedAttribute"/>
<!-- other permissions -->
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<!-- android.permission-group.CONTACTS -->
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
@ -137,7 +140,7 @@
<action android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED"/>
</intent-filter>
</receiver>
<activity
android:name=".ui.AccountsActivity"
android:label="@string/app_name"
@ -159,6 +162,10 @@
android:label="@string/app_settings"
android:parentActivityName=".ui.AccountsActivity"/>
<activity android:name=".ui.PermissionsActivity"
android:label="@string/permissions_title"
android:parentActivityName=".ui.AccountsActivity"/>
<activity
android:name=".ui.setup.LoginActivity"
android:label="@string/login_title"

@ -21,7 +21,8 @@ public class Constants {
NOTIFICATION_REFRESH_COLLECTIONS = 2,
NOTIFICATION_CONTACTS_SYNC = 10,
NOTIFICATION_CALENDAR_SYNC = 11,
NOTIFICATION_TASK_SYNC = 12;
NOTIFICATION_TASK_SYNC = 12,
NOTIFICATION_PERMISSIONS = 20;
public static final Uri webUri = Uri.parse("https://davdroid.bitfire.at/?pk_campaign=davdroid-app");

@ -72,20 +72,22 @@ public class DavService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
long id = intent.getLongExtra(EXTRA_DAV_SERVICE_ID, -1);
switch (action) {
case ACTION_ACCOUNTS_UPDATED:
cleanupAccounts();
break;
case ACTION_REFRESH_COLLECTIONS:
if (runningRefresh.add(id)) {
new Thread(new RefreshCollections(id)).start();
for (RefreshingStatusListener listener : refreshingStatusListeners)
listener.onDavRefreshStatusChanged(id, true);
}
break;
if (intent != null) {
String action = intent.getAction();
long id = intent.getLongExtra(EXTRA_DAV_SERVICE_ID, -1);
switch (action) {
case ACTION_ACCOUNTS_UPDATED:
cleanupAccounts();
break;
case ACTION_REFRESH_COLLECTIONS:
if (runningRefresh.add(id)) {
new Thread(new RefreshCollections(id)).start();
for (RefreshingStatusListener listener : refreshingStatusListeners)
listener.onDavRefreshStatusChanged(id, true);
}
break;
}
}
return START_NOT_STICKY;

@ -9,10 +9,11 @@
package at.bitfire.davdroid.resource;
import android.accounts.Account;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.text.TextUtils;
@ -29,7 +30,6 @@ import at.bitfire.ical4android.AndroidTaskListFactory;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.TaskProvider;
import lombok.Cleanup;
import okhttp3.HttpUrl;
public class LocalTaskList extends AndroidTaskList implements LocalCollection {
@ -134,12 +134,16 @@ public class LocalTaskList extends AndroidTaskList implements LocalCollection {
// helpers
public static boolean tasksProviderAvailable(@NonNull ContentResolver resolver) {
public static boolean tasksProviderAvailable(@NonNull Context context) {
if (tasksProviderAvailable != null)
return tasksProviderAvailable;
else {
@Cleanup TaskProvider provider = TaskProvider.acquire(resolver, TaskProvider.ProviderName.OpenTasks);
return tasksProviderAvailable = (provider != null);
if (Build.VERSION.SDK_INT >= 23)
return context.getPackageManager().resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null;
else {
@Cleanup TaskProvider provider = TaskProvider.acquire(context.getContentResolver(), TaskProvider.ProviderName.OpenTasks);
return tasksProviderAvailable = (provider != null);
}
}
}

@ -9,13 +9,16 @@
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.drawable.BitmapDrawable;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
@ -24,18 +27,15 @@ import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.net.ConnectivityManagerCompat;
import android.text.TextUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.WordUtils;
import android.support.v4.app.NotificationCompat;
import java.util.logging.Level;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.ui.PermissionsActivity;
public abstract class SyncAdapterService extends Service {
@ -62,6 +62,27 @@ public abstract class SyncAdapterService extends Service {
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
}
@Override
public void onSecurityException(Account account, Bundle extras, String authority, SyncResult syncResult) {
App.log.log(Level.WARNING, "Security exception when opening content provider for " + authority);
syncResult.databaseError = true;
Intent intent = new Intent(getContext(), PermissionsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Notification notify = new NotificationCompat.Builder(getContext())
.setSmallIcon(R.drawable.ic_error_light)
.setLargeIcon(((BitmapDrawable)getContext().getResources().getDrawable(R.drawable.ic_launcher)).getBitmap())
.setContentTitle("DAVdroid permissions")
.setContentText("Additional permissions are required.")
.setContentIntent(PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_CANCEL_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setLocalOnly(true)
.build();
NotificationManager nm = (NotificationManager)getContext().getSystemService(NOTIFICATION_SERVICE);
nm.notify(Constants.NOTIFICATION_PERMISSIONS, notify);
}
protected boolean checkSyncConditions(@NonNull AccountSettings settings) {
if (settings.getSyncWifiOnly()) {
ConnectivityManager cm = (ConnectivityManager)getContext().getSystemService(CONNECTIVITY_SERVICE);

@ -15,6 +15,7 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.NotificationCompat;
@ -223,7 +224,8 @@ abstract public class SyncManager {
detailsIntent.setData(Uri.parse("uri://" + getClass().getName() + "/" + uniqueCollectionId));
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
builder .setSmallIcon(R.drawable.ic_launcher)
builder .setSmallIcon(R.drawable.ic_error_light)
.setLargeIcon(((BitmapDrawable)context.getResources().getDrawable(R.drawable.ic_launcher)).getBitmap())
.setContentTitle(getSyncErrorTitle())
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)

@ -0,0 +1,95 @@
/*
* Copyright © 2013 2016 Ricki Hirner (bitfire web engineering).
* 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
*/
package at.bitfire.davdroid.ui;
import android.Manifest;
import android.app.NotificationManager;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalTaskList;
public class PermissionsActivity extends AppCompatActivity {
public static final String
PERMISSION_READ_TASKS = "org.dmfs.permission.READ_TASKS",
PERMISSION_WRITE_TASKS = "org.dmfs.permission.WRITE_TASKS";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_permissions);
refresh();
}
protected void refresh() {
boolean noCalendarPermissions =
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED;
findViewById(R.id.calendar_permissions).setVisibility(noCalendarPermissions ? View.VISIBLE : View.GONE);
boolean noContactsPermissions =
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED;
findViewById(R.id.contacts_permissions).setVisibility(noContactsPermissions ? View.VISIBLE : View.GONE);
boolean noTaskPermissions;
if (LocalTaskList.tasksProviderAvailable(this)) {
noTaskPermissions =
ActivityCompat.checkSelfPermission(this, PERMISSION_READ_TASKS) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, PERMISSION_WRITE_TASKS) != PackageManager.PERMISSION_GRANTED;
findViewById(R.id.opentasks_permissions).setVisibility(noTaskPermissions ? View.VISIBLE : View.GONE);
} else {
findViewById(R.id.opentasks_permissions).setVisibility(View.GONE);
noTaskPermissions = false;
}
if (!noCalendarPermissions && !noContactsPermissions && !noTaskPermissions) {
NotificationManager nm = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
nm.cancel(Constants.NOTIFICATION_PERMISSIONS);
finish();
}
}
public void requestCalendarPermissions(View v) {
ActivityCompat.requestPermissions(this, new String[] {
Manifest.permission.READ_CALENDAR,
Manifest.permission.WRITE_CALENDAR
}, 0);
}
public void requestContactsPermissions(View v) {
ActivityCompat.requestPermissions(this, new String[] {
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_CONTACTS
}, 0);
}
public void requestOpenTasksPermissions(View v) {
ActivityCompat.requestPermissions(this, new String[] {
PERMISSION_READ_TASKS,
PERMISSION_WRITE_TASKS
}, 0);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
refresh();
}
}

@ -70,7 +70,7 @@ public class StartupDialogFragment extends DialogFragment {
}
// OpenTasks information
if (!LocalTaskList.tasksProviderAvailable(context.getContentResolver()) &&
if (!LocalTaskList.tasksProviderAvailable(context) &&
settings.getBoolean(HINT_OPENTASKS_NOT_INSTALLED, true))
dialogs.add(StartupDialogFragment.instantiate(Mode.OPENTASKS_NOT_INSTALLED));
@ -177,7 +177,8 @@ public class StartupDialogFragment extends DialogFragment {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=org.dmfs.tasks"));
getContext().startActivity(intent);
if (intent.resolveActivity(getContext().getPackageManager()) != null)
getContext().startActivity(intent);
}
})
.setNegativeButton(R.string.startup_dont_show_again, new DialogInterface.OnClickListener() {

@ -14,6 +14,7 @@ import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
@ -107,13 +108,13 @@ public class AccountDetailsFragment extends Fragment {
App.log.log(Level.INFO, "Writing account configuration to database", config);
@Cleanup OpenHelper dbHelper = new OpenHelper(getContext());
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransactionNonExclusive();
try {
AccountSettings settings = new AccountSettings(getContext(), account);
Intent refreshIntent = new Intent(getActivity(), DavService.class);
refreshIntent.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
db.beginTransactionNonExclusive();
if (config.cardDAV != null) {
long id = insertService(db, accountName, Services.SERVICE_CARDDAV, config.cardDAV);
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id);
@ -132,13 +133,12 @@ public class AccountDetailsFragment extends Fragment {
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1);
settings.setSyncInterval(CalendarContract.AUTHORITY, DEFAULT_SYNC_INTERVAL);
if (LocalTaskList.tasksProviderAvailable(getContext().getContentResolver())) {
// will only do something if OpenTasks is installed and accessible
if (Build.VERSION.SDK_INT >= 23 || LocalTaskList.tasksProviderAvailable(getContext())) {
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1);
settings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, DEFAULT_SYNC_INTERVAL);
} else
// If OpenTasks is installed after DAVdroid, DAVdroid won't get task permissions and crash at every task sync
// unless we disable task sync here (before OpenTasks is available).
// Android <6 only: disable OpenTasks sync forever when OpenTasks is not installed
// because otherwise, there will be a non-catchable SecurityException as soon as OpenTasks is installed
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0);
} else {
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0);

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2013 2016 Ricki Hirner (bitfire web engineering).
~ 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
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/activity_margin">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/calendar_permissions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp"
tools:ignore="UselessParent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextView.Heading"
android:text="@string/permissions_calendar"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/permissions_calendar_details"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/permissions_calendar_request"
android:onClick="requestCalendarPermissions"/>
</LinearLayout>
<LinearLayout
android:id="@+id/contacts_permissions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp"
tools:ignore="UselessParent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextView.Heading"
android:text="@string/permissions_contacts"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/permissions_contacts_details"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/permissions_contacts_request"
android:onClick="requestContactsPermissions"/>
</LinearLayout>
<LinearLayout
android:id="@+id/opentasks_permissions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:ignore="UselessParent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextView.Heading"
android:text="@string/permissions_opentasks"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/permissions_opentasks_details"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/permissions_opentasks_request"
android:onClick="requestOpenTasksPermissions"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

@ -94,6 +94,18 @@
<string name="account_refresh_calendar_list">Refresh calendar list</string>
<string name="account_create_new_calendar">Create new calendar</string>
<!-- PermissionsActivity -->
<string name="permissions_title">DAVdroid permissions</string>
<string name="permissions_calendar">Calendar permissions</string>
<string name="permissions_calendar_details">To synchronize CalDAV events with your local calendars, DAVdroid needs to access your calendars.</string>
<string name="permissions_calendar_request">Request calendar permissions</string>
<string name="permissions_contacts">Contacts permissions</string>
<string name="permissions_contacts_details">To synchronize CardDAV address books with your local contacts, DAVdroid needs to access your contacts.</string>
<string name="permissions_contacts_request">Request contacts permissions</string>
<string name="permissions_opentasks">OpenTasks permissions</string>
<string name="permissions_opentasks_details">To synchronize CalDAV tasks with your local task lists, DAVdroid needs to access OpenTasks.</string>
<string name="permissions_opentasks_request">Request OpenTasks permissions</string>
<!-- AddAccountActivity -->
<string name="login_title">Add account</string>
<string name="login_type_email">Login with email address</string>
@ -159,13 +171,6 @@
<item>Every 4 hours</item>
<item>Once a day</item>
</string-array>
<string name="settings_sync_time_range_past">Past event time limit</string>
<string name="settings_sync_time_range_past_none">All events will be synchronized</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Events more than one day in the past will be ignored</item>
<item quantity="other">Events more than %d days in the past will be ignored</item>
</plurals>
<string name="settings_sync_time_range_past_message">Events which are more than this number of days in the past will be ignored (may be 0). Leave blank to synchronize all events.</string>
<string name="settings_sync_wifi_only">Sync over WiFi only</string>
<string name="settings_sync_wifi_only_on">Synchronization is restricted to WiFi connections</string>
<string name="settings_sync_wifi_only_off">Connection type is not taken into consideration</string>
@ -173,6 +178,14 @@
<string name="settings_sync_wifi_only_ssid_on">Will only synchronize over %s</string>
<string name="settings_sync_wifi_only_ssid_off">All WiFi connections may be used</string>
<string name="settings_sync_wifi_only_ssid_message">Enter the name of a WiFi network (SSID) to restrict synchronization to this network, or leave blank for all WiFi connections.</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Past event time limit</string>
<string name="settings_sync_time_range_past_none">All events will be synchronized</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Events more than one day in the past will be ignored</item>
<item quantity="other">Events more than %d days in the past will be ignored</item>
</plurals>
<string name="settings_sync_time_range_past_message">Events which are more than this number of days in the past will be ignored (may be 0). Leave blank to synchronize all events.</string>
<string name="settings_manage_calendar_colors">Manage calendar colors</string>
<string name="settings_manage_calendar_colors_on">Calendar colors are managed by DAVdroid</string>
<string name="settings_manage_calendar_colors_off">Calendar colors are not set by DAVdroid</string>

@ -74,7 +74,7 @@
</PreferenceCategory>
<PreferenceCategory android:title="CalDAV">
<PreferenceCategory android:title="@string/settings_caldav">
<EditTextPreference
android:key="caldav_time_range_past_days"

@ -12,7 +12,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0+'
classpath 'com.android.tools.build:gradle:2.0.0+'
}
}

@ -1,6 +1,6 @@
#Fri Oct 30 11:00:03 CET 2015
#Sat Apr 09 22:18:11 CEST 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip

@ -1 +1 @@
Subproject commit 19ef2603edc7a28bce1b9ea1f7fa11ca89234dc0
Subproject commit a9495e136733e44f3276f08c049def2584aac3a4

@ -1 +1 @@
Subproject commit 4eab186a210f7fdb74f17af2b086fc17ba2291c7
Subproject commit 80b0f298076766df5844c5209a260423ebd1a8d3
Loading…
Cancel
Save