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

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

@ -21,7 +21,8 @@ public class Constants {
NOTIFICATION_REFRESH_COLLECTIONS = 2, NOTIFICATION_REFRESH_COLLECTIONS = 2,
NOTIFICATION_CONTACTS_SYNC = 10, NOTIFICATION_CONTACTS_SYNC = 10,
NOTIFICATION_CALENDAR_SYNC = 11, 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"); 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 @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction(); if (intent != null) {
long id = intent.getLongExtra(EXTRA_DAV_SERVICE_ID, -1); String action = intent.getAction();
long id = intent.getLongExtra(EXTRA_DAV_SERVICE_ID, -1);
switch (action) {
case ACTION_ACCOUNTS_UPDATED: switch (action) {
cleanupAccounts(); case ACTION_ACCOUNTS_UPDATED:
break; cleanupAccounts();
case ACTION_REFRESH_COLLECTIONS: break;
if (runningRefresh.add(id)) { case ACTION_REFRESH_COLLECTIONS:
new Thread(new RefreshCollections(id)).start(); if (runningRefresh.add(id)) {
for (RefreshingStatusListener listener : refreshingStatusListeners) new Thread(new RefreshCollections(id)).start();
listener.onDavRefreshStatusChanged(id, true); for (RefreshingStatusListener listener : refreshingStatusListeners)
} listener.onDavRefreshStatusChanged(id, true);
break; }
break;
}
} }
return START_NOT_STICKY; return START_NOT_STICKY;

@ -9,10 +9,11 @@
package at.bitfire.davdroid.resource; package at.bitfire.davdroid.resource;
import android.accounts.Account; import android.accounts.Account;
import android.content.ContentResolver;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.RemoteException; import android.os.RemoteException;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
@ -29,7 +30,6 @@ import at.bitfire.ical4android.AndroidTaskListFactory;
import at.bitfire.ical4android.CalendarStorageException; import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.TaskProvider; import at.bitfire.ical4android.TaskProvider;
import lombok.Cleanup; import lombok.Cleanup;
import okhttp3.HttpUrl;
public class LocalTaskList extends AndroidTaskList implements LocalCollection { public class LocalTaskList extends AndroidTaskList implements LocalCollection {
@ -134,12 +134,16 @@ public class LocalTaskList extends AndroidTaskList implements LocalCollection {
// helpers // helpers
public static boolean tasksProviderAvailable(@NonNull ContentResolver resolver) { public static boolean tasksProviderAvailable(@NonNull Context context) {
if (tasksProviderAvailable != null) if (tasksProviderAvailable != null)
return tasksProviderAvailable; return tasksProviderAvailable;
else { else {
@Cleanup TaskProvider provider = TaskProvider.acquire(resolver, TaskProvider.ProviderName.OpenTasks); if (Build.VERSION.SDK_INT >= 23)
return tasksProviderAvailable = (provider != null); 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; package at.bitfire.davdroid.syncadapter;
import android.accounts.Account; import android.accounts.Account;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.AbstractThreadedSyncAdapter; import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient; import android.content.ContentProviderClient;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SyncResult; import android.content.SyncResult;
import android.database.sqlite.SQLiteDatabase; import android.graphics.drawable.BitmapDrawable;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.net.wifi.WifiInfo; import android.net.wifi.WifiInfo;
@ -24,18 +27,15 @@ import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.net.ConnectivityManagerCompat; import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.WordUtils;
import java.util.logging.Level; import java.util.logging.Level;
import at.bitfire.davdroid.AccountSettings; import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App; import at.bitfire.davdroid.App;
import at.bitfire.davdroid.InvalidAccountException; import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.model.ServiceDB; import at.bitfire.davdroid.R;
import at.bitfire.davdroid.ui.PermissionsActivity;
public abstract class SyncAdapterService extends Service { public abstract class SyncAdapterService extends Service {
@ -62,6 +62,27 @@ public abstract class SyncAdapterService extends Service {
Thread.currentThread().setContextClassLoader(getContext().getClassLoader()); 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) { protected boolean checkSyncConditions(@NonNull AccountSettings settings) {
if (settings.getSyncWifiOnly()) { if (settings.getSyncWifiOnly()) {
ConnectivityManager cm = (ConnectivityManager)getContext().getSystemService(CONNECTIVITY_SERVICE); ConnectivityManager cm = (ConnectivityManager)getContext().getSystemService(CONNECTIVITY_SERVICE);

@ -15,6 +15,7 @@ import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SyncResult; import android.content.SyncResult;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.NotificationCompat; import android.support.v7.app.NotificationCompat;
@ -223,7 +224,8 @@ abstract public class SyncManager {
detailsIntent.setData(Uri.parse("uri://" + getClass().getName() + "/" + uniqueCollectionId)); detailsIntent.setData(Uri.parse("uri://" + getClass().getName() + "/" + uniqueCollectionId));
NotificationCompat.Builder builder = new NotificationCompat.Builder(context); 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()) .setContentTitle(getSyncErrorTitle())
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT)) .setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR) .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 // OpenTasks information
if (!LocalTaskList.tasksProviderAvailable(context.getContentResolver()) && if (!LocalTaskList.tasksProviderAvailable(context) &&
settings.getBoolean(HINT_OPENTASKS_NOT_INSTALLED, true)) settings.getBoolean(HINT_OPENTASKS_NOT_INSTALLED, true))
dialogs.add(StartupDialogFragment.instantiate(Mode.OPENTASKS_NOT_INSTALLED)); dialogs.add(StartupDialogFragment.instantiate(Mode.OPENTASKS_NOT_INSTALLED));
@ -177,7 +177,8 @@ public class StartupDialogFragment extends DialogFragment {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=org.dmfs.tasks")); 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() { .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.ContentValues;
import android.content.Intent; import android.content.Intent;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.CalendarContract; import android.provider.CalendarContract;
import android.provider.ContactsContract; import android.provider.ContactsContract;
@ -107,13 +108,13 @@ public class AccountDetailsFragment extends Fragment {
App.log.log(Level.INFO, "Writing account configuration to database", config); App.log.log(Level.INFO, "Writing account configuration to database", config);
@Cleanup OpenHelper dbHelper = new OpenHelper(getContext()); @Cleanup OpenHelper dbHelper = new OpenHelper(getContext());
SQLiteDatabase db = dbHelper.getWritableDatabase(); SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransactionNonExclusive();
try { try {
AccountSettings settings = new AccountSettings(getContext(), account); AccountSettings settings = new AccountSettings(getContext(), account);
Intent refreshIntent = new Intent(getActivity(), DavService.class); Intent refreshIntent = new Intent(getActivity(), DavService.class);
refreshIntent.setAction(DavService.ACTION_REFRESH_COLLECTIONS); refreshIntent.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
db.beginTransactionNonExclusive();
if (config.cardDAV != null) { if (config.cardDAV != null) {
long id = insertService(db, accountName, Services.SERVICE_CARDDAV, config.cardDAV); long id = insertService(db, accountName, Services.SERVICE_CARDDAV, config.cardDAV);
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id); refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id);
@ -132,13 +133,12 @@ public class AccountDetailsFragment extends Fragment {
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1); ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1);
settings.setSyncInterval(CalendarContract.AUTHORITY, DEFAULT_SYNC_INTERVAL); settings.setSyncInterval(CalendarContract.AUTHORITY, DEFAULT_SYNC_INTERVAL);
if (LocalTaskList.tasksProviderAvailable(getContext().getContentResolver())) { if (Build.VERSION.SDK_INT >= 23 || LocalTaskList.tasksProviderAvailable(getContext())) {
// will only do something if OpenTasks is installed and accessible
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1); ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1);
settings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, DEFAULT_SYNC_INTERVAL); settings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, DEFAULT_SYNC_INTERVAL);
} else } else
// If OpenTasks is installed after DAVdroid, DAVdroid won't get task permissions and crash at every task sync // Android <6 only: disable OpenTasks sync forever when OpenTasks is not installed
// unless we disable task sync here (before OpenTasks is available). // because otherwise, there will be a non-catchable SecurityException as soon as OpenTasks is installed
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0); ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0);
} else { } else {
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0); 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_refresh_calendar_list">Refresh calendar list</string>
<string name="account_create_new_calendar">Create new calendar</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 --> <!-- AddAccountActivity -->
<string name="login_title">Add account</string> <string name="login_title">Add account</string>
<string name="login_type_email">Login with email address</string> <string name="login_type_email">Login with email address</string>
@ -159,13 +171,6 @@
<item>Every 4 hours</item> <item>Every 4 hours</item>
<item>Once a day</item> <item>Once a day</item>
</string-array> </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">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_on">Synchronization is restricted to WiFi connections</string>
<string name="settings_sync_wifi_only_off">Connection type is not taken into consideration</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_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_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_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">Manage calendar colors</string>
<string name="settings_manage_calendar_colors_on">Calendar colors are managed by DAVdroid</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> <string name="settings_manage_calendar_colors_off">Calendar colors are not set by DAVdroid</string>

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

@ -12,7 +12,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { 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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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