diff --git a/app/build.gradle b/app/build.gradle index c1bbf96b..6c1d3da1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d46b7b1a..c2970f9a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,10 @@ + - @@ -137,7 +140,7 @@ - + + + = 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); + } } } diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncAdapterService.java index 447c0189..7f75774b 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncAdapterService.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncAdapterService.java @@ -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); diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java index 1e80e554..84920bc0 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java @@ -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) diff --git a/app/src/main/java/at/bitfire/davdroid/ui/PermissionsActivity.java b/app/src/main/java/at/bitfire/davdroid/ui/PermissionsActivity.java new file mode 100644 index 00000000..a17490ec --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ui/PermissionsActivity.java @@ -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(); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/ui/StartupDialogFragment.java b/app/src/main/java/at/bitfire/davdroid/ui/StartupDialogFragment.java index 70d0bbd0..cbb32c2e 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/StartupDialogFragment.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/StartupDialogFragment.java @@ -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() { diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.java b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.java index 21b3bbb2..9271dc07 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.java @@ -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); diff --git a/app/src/main/res/layout/activity_permissions.xml b/app/src/main/res/layout/activity_permissions.xml new file mode 100644 index 00000000..bd86b039 --- /dev/null +++ b/app/src/main/res/layout/activity_permissions.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + +