Merge pull request #3632 from GoneUp/runtime_contacts
Runtime permissions for contacts
This commit is contained in:
commit
0455157eb7
11 changed files with 276 additions and 26 deletions
|
@ -1,14 +1,18 @@
|
|||
package com.fsck.k9.helper;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.AbstractCursor;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import timber.log.Timber;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Photo;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
||||
import com.fsck.k9.mail.Address;
|
||||
|
||||
|
@ -263,6 +267,14 @@ public class Contacts {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean hasContactPermission() {
|
||||
boolean canRead = ContextCompat.checkSelfPermission(mContext,
|
||||
Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
|
||||
boolean canWrite = ContextCompat.checkSelfPermission(mContext,
|
||||
Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED;
|
||||
return canRead && canWrite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link Cursor} instance that can be used to fetch information
|
||||
* about the contact with the given email address.
|
||||
|
@ -273,12 +285,17 @@ public class Contacts {
|
|||
*/
|
||||
private Cursor getContactByAddress(final String address) {
|
||||
final Uri uri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, Uri.encode(address));
|
||||
|
||||
if (hasContactPermission()) {
|
||||
return mContentResolver.query(
|
||||
uri,
|
||||
PROJECTION,
|
||||
null,
|
||||
null,
|
||||
SORT_ORDER);
|
||||
} else {
|
||||
return new EmptyCursor();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
54
app/core/src/main/java/com/fsck/k9/helper/EmptyCursor.java
Normal file
54
app/core/src/main/java/com/fsck/k9/helper/EmptyCursor.java
Normal file
|
@ -0,0 +1,54 @@
|
|||
package com.fsck.k9.helper;
|
||||
|
||||
import android.database.AbstractCursor;
|
||||
|
||||
|
||||
/**
|
||||
* A dummy class that provides a empty cursor
|
||||
*/
|
||||
public class EmptyCursor extends AbstractCursor {
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getColumnNames() {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(int column) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getShort(int column) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(int column) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(int column) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(int column) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(int column) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNull(int column) {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ android {
|
|||
versionName '5.700-SNAPSHOT'
|
||||
|
||||
minSdkVersion buildConfig.minSdk
|
||||
targetSdkVersion 22
|
||||
targetSdkVersion 23
|
||||
|
||||
generatedDensities = ['mdpi', 'hdpi', 'xhdpi']
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion buildConfig.minSdk
|
||||
// For Robolectric tests
|
||||
targetSdkVersion 22
|
||||
targetSdkVersion 23
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
|
|
|
@ -1,14 +1,27 @@
|
|||
package com.fsck.k9.activity;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import com.fsck.k9.activity.K9ActivityCommon.K9ActivityMagic;
|
||||
import com.fsck.k9.activity.misc.SwipeGestureDetector.OnSwipeGestureListener;
|
||||
import com.fsck.k9.ui.R;
|
||||
import com.fsck.k9.ui.permissions.PermissionRationaleDialogFragment;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public abstract class K9Activity extends AppCompatActivity implements K9ActivityMagic {
|
||||
public static final int PERMISSIONS_REQUEST_READ_CONTACTS = 1;
|
||||
public static final int PERMISSIONS_REQUEST_WRITE_CONTACTS = 2;
|
||||
private static final String FRAGMENT_TAG_RATIONALE = "rationale";
|
||||
|
||||
|
||||
private K9ActivityCommon mBase;
|
||||
|
||||
|
@ -29,4 +42,53 @@ public abstract class K9Activity extends AppCompatActivity implements K9Activity
|
|||
public void setupGestureDetector(OnSwipeGestureListener listener) {
|
||||
mBase.setupGestureDetector(listener);
|
||||
}
|
||||
|
||||
public boolean hasPermission(Permission permission) {
|
||||
return ContextCompat.checkSelfPermission(this, permission.permission) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
public void requestPermissionOrShowRationale(Permission permission) {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission.permission)) {
|
||||
PermissionRationaleDialogFragment dialogFragment =
|
||||
PermissionRationaleDialogFragment.newInstance(permission);
|
||||
|
||||
dialogFragment.show(getSupportFragmentManager(), FRAGMENT_TAG_RATIONALE);
|
||||
} else {
|
||||
requestPermission(permission);
|
||||
}
|
||||
}
|
||||
|
||||
public void requestPermission(Permission permission) {
|
||||
Timber.i("Requesting permission: " + permission.permission);
|
||||
ActivityCompat.requestPermissions(this, new String[] { permission.permission }, permission.requestCode);
|
||||
}
|
||||
|
||||
|
||||
public enum Permission {
|
||||
READ_CONTACTS(
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
PERMISSIONS_REQUEST_READ_CONTACTS,
|
||||
R.string.permission_contacts_rationale_title,
|
||||
R.string.permission_contacts_rationale_message
|
||||
),
|
||||
WRITE_CONTACTS(
|
||||
Manifest.permission.WRITE_CONTACTS,
|
||||
PERMISSIONS_REQUEST_WRITE_CONTACTS,
|
||||
R.string.permission_contacts_rationale_title,
|
||||
R.string.permission_contacts_rationale_message
|
||||
);
|
||||
|
||||
|
||||
public final String permission;
|
||||
public final int requestCode;
|
||||
public final int rationaleTitle;
|
||||
public final int rationaleMessage;
|
||||
|
||||
Permission(String permission, int requestCode, @StringRes int rationaleTitle, @StringRes int rationaleMessage) {
|
||||
this.permission = permission;
|
||||
this.requestCode = requestCode;
|
||||
this.rationaleTitle = rationaleTitle;
|
||||
this.rationaleMessage = rationaleMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,8 +48,6 @@ import com.fsck.k9.Account.MessageFormat;
|
|||
import com.fsck.k9.Identity;
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.Preferences;
|
||||
import com.fsck.k9.controller.MessageReference;
|
||||
import com.fsck.k9.ui.R;
|
||||
import com.fsck.k9.activity.MessageLoaderHelper.MessageLoaderCallbacks;
|
||||
import com.fsck.k9.activity.compose.AttachmentPresenter;
|
||||
import com.fsck.k9.activity.compose.AttachmentPresenter.AttachmentMvpView;
|
||||
|
@ -65,6 +63,7 @@ import com.fsck.k9.activity.compose.RecipientMvpView;
|
|||
import com.fsck.k9.activity.compose.RecipientPresenter;
|
||||
import com.fsck.k9.activity.compose.SaveMessageTask;
|
||||
import com.fsck.k9.activity.misc.Attachment;
|
||||
import com.fsck.k9.controller.MessageReference;
|
||||
import com.fsck.k9.controller.MessagingController;
|
||||
import com.fsck.k9.controller.MessagingListener;
|
||||
import com.fsck.k9.controller.SimpleMessagingListener;
|
||||
|
@ -98,6 +97,7 @@ import com.fsck.k9.message.SimpleMessageBuilder;
|
|||
import com.fsck.k9.message.SimpleMessageFormat;
|
||||
import com.fsck.k9.search.LocalSearch;
|
||||
import com.fsck.k9.ui.EolConvertingEditText;
|
||||
import com.fsck.k9.ui.R;
|
||||
import com.fsck.k9.ui.compose.QuotedMessageMvpView;
|
||||
import com.fsck.k9.ui.compose.QuotedMessagePresenter;
|
||||
import org.openintents.openpgp.OpenPgpApiManager;
|
||||
|
@ -456,6 +456,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
|
|||
setProgressBarIndeterminateVisibility(true);
|
||||
currentMessageBuilder.reattachCallback(this);
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
checkAndRequestPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -639,6 +643,12 @@ public class MessageCompose extends K9Activity implements OnClickListener,
|
|||
updateMessageFormat();
|
||||
}
|
||||
|
||||
private void checkAndRequestPermissions() {
|
||||
if (!hasPermission(Permission.READ_CONTACTS)) {
|
||||
requestPermissionOrShowRationale(Permission.READ_CONTACTS);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTitle() {
|
||||
setTitle(action.getTitleResource());
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package com.fsck.k9.activity;
|
||||
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
|
@ -63,14 +66,11 @@ import com.fsck.k9.view.MessageTitleView;
|
|||
import com.fsck.k9.view.ViewSwitcher;
|
||||
import com.fsck.k9.view.ViewSwitcher.OnSwitchCompleteListener;
|
||||
import com.mikepenz.materialdrawer.Drawer.OnDrawerListener;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import de.cketti.library.changelog.ChangeLog;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* MessageList is the primary user interface for the program. This Activity
|
||||
* shows a list of messages.
|
||||
|
@ -253,6 +253,10 @@ public class MessageList extends K9Activity implements MessageListFragmentListen
|
|||
if (cl.isFirstRun()) {
|
||||
cl.getLogDialog().show();
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
checkAndRequestPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -491,6 +495,13 @@ public class MessageList extends K9Activity implements MessageListFragmentListen
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
private void checkAndRequestPermissions() {
|
||||
if (!hasPermission(Permission.READ_CONTACTS)) {
|
||||
requestPermissionOrShowRationale(Permission.READ_CONTACTS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
|
|
@ -8,8 +8,10 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
|
@ -19,7 +21,9 @@ import android.provider.ContactsContract.Contacts.Data;
|
|||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
||||
import com.fsck.k9.helper.EmptyCursor;
|
||||
import com.fsck.k9.ui.R;
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.view.RecipientSelectView.Recipient;
|
||||
|
@ -285,18 +289,30 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
|
|||
return contactUri.getLastPathSegment();
|
||||
}
|
||||
|
||||
private boolean hasContactPermission() {
|
||||
boolean canRead = ContextCompat.checkSelfPermission(getContext(),
|
||||
Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
|
||||
boolean canWrite = ContextCompat.checkSelfPermission(getContext(),
|
||||
Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED;
|
||||
return canRead && canWrite;
|
||||
}
|
||||
|
||||
private Cursor getNicknameCursor(String nickname) {
|
||||
nickname = "%" + nickname + "%";
|
||||
|
||||
Uri queryUriForNickname = ContactsContract.Data.CONTENT_URI;
|
||||
|
||||
|
||||
if (hasContactPermission()) {
|
||||
return contentResolver.query(queryUriForNickname,
|
||||
PROJECTION_NICKNAME,
|
||||
ContactsContract.CommonDataKinds.Nickname.NAME + " LIKE ? AND " +
|
||||
Data.MIMETYPE + " = ?",
|
||||
new String[] { nickname, ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE },
|
||||
null);
|
||||
} else {
|
||||
return new EmptyCursor();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
|
@ -358,7 +374,11 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
|
|||
List<Recipient> recipients = new ArrayList<>();
|
||||
|
||||
Uri queryUri = Email.CONTENT_URI;
|
||||
Cursor cursor = contentResolver.query(queryUri, PROJECTION, null, null, SORT_ORDER);
|
||||
|
||||
Cursor cursor = null;
|
||||
if (hasContactPermission()) {
|
||||
cursor = contentResolver.query(queryUri, PROJECTION, null, null, SORT_ORDER);
|
||||
}
|
||||
|
||||
if (cursor == null) {
|
||||
return recipients;
|
||||
|
@ -380,7 +400,11 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
|
|||
String selection = Contacts.DISPLAY_NAME_PRIMARY + " LIKE ? " +
|
||||
" OR (" + Email.ADDRESS + " LIKE ? AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "')";
|
||||
String[] selectionArgs = { query, query };
|
||||
Cursor cursor = contentResolver.query(queryUri, PROJECTION, selection, selectionArgs, SORT_ORDER);
|
||||
|
||||
Cursor cursor = null;
|
||||
if (hasContactPermission()) {
|
||||
cursor = contentResolver.query(queryUri, PROJECTION, selection, selectionArgs, SORT_ORDER);
|
||||
}
|
||||
|
||||
if (cursor == null) {
|
||||
return false;
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package com.fsck.k9.ui.permissions
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import androidx.core.os.bundleOf
|
||||
import com.fsck.k9.activity.K9Activity
|
||||
import com.fsck.k9.activity.K9Activity.Permission
|
||||
import com.fsck.k9.ui.R
|
||||
|
||||
/**
|
||||
* A dialog displaying a message to explain why the app requests a certain permission.
|
||||
*
|
||||
* Closing the dialog triggers a permission request. For this to work the Activity needs to be a subclass of
|
||||
* [K9Activity].
|
||||
*/
|
||||
class PermissionRationaleDialogFragment : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val args = arguments!!
|
||||
val permissionName = args.getString(ARG_PERMISSION)
|
||||
|
||||
val permission = Permission.valueOf(permissionName)
|
||||
|
||||
return AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(permission.rationaleTitle)
|
||||
setMessage(permission.rationaleMessage)
|
||||
setPositiveButton(R.string.okay_action) { _, _ ->
|
||||
val activity = requireActivity() as? K9Activity
|
||||
?: throw AssertionError("PermissionRationaleDialogFragment can only be used with K9Activity")
|
||||
|
||||
activity.requestPermission(permission)
|
||||
}
|
||||
}.create()
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val ARG_PERMISSION = "permission"
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(permission: Permission): PermissionRationaleDialogFragment {
|
||||
return PermissionRationaleDialogFragment().apply {
|
||||
arguments = bundleOf(ARG_PERMISSION to permission.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1350,4 +1350,9 @@ You can keep this message and use it as a backup for your secret key. If you wan
|
|||
|
||||
<string name="navigation_drawer_open">Open</string>
|
||||
<string name="navigation_drawer_close">Close</string>
|
||||
|
||||
|
||||
<!-- permissions -->
|
||||
<string name="permission_contacts_rationale_title">Allow access to contacts</string>
|
||||
<string name="permission_contacts_rationale_message">To be able to provide contact suggestions and to display contact names and photos, the app needs access to your contacts.</string>
|
||||
</resources>
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.fsck.k9.activity.compose;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.MatrixCursor;
|
||||
|
@ -16,7 +17,7 @@ import com.fsck.k9.view.RecipientSelectView.Recipient;
|
|||
import com.fsck.k9.view.RecipientSelectView.RecipientCryptoStatus;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.shadows.ShadowApplication;
|
||||
|
||||
import static android.provider.ContactsContract.CommonDataKinds.Email.TYPE_HOME;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
@ -65,18 +66,21 @@ public class RecipientLoaderTest extends RobolectricTest {
|
|||
|
||||
static final String QUERYSTRING = "querystring";
|
||||
|
||||
|
||||
ShadowApplication shadowApp;
|
||||
Context context;
|
||||
ContentResolver contentResolver;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
shadowApp = ShadowApplication.getInstance();
|
||||
shadowApp.grantPermissions(Manifest.permission.READ_CONTACTS);
|
||||
shadowApp.grantPermissions(Manifest.permission.WRITE_CONTACTS);
|
||||
|
||||
context = mock(Context.class);
|
||||
contentResolver = mock(ContentResolver.class);
|
||||
|
||||
when(context.getApplicationContext()).thenReturn(RuntimeEnvironment.application);
|
||||
|
||||
when(context.getApplicationContext()).thenReturn(shadowApp.getApplicationContext());
|
||||
when(context.getContentResolver()).thenReturn(contentResolver);
|
||||
}
|
||||
|
||||
|
@ -213,6 +217,7 @@ public class RecipientLoaderTest extends RobolectricTest {
|
|||
nullable(String.class))).thenReturn(cursor);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void queryContactProvider() throws Exception {
|
||||
RecipientLoader recipientLoader = new RecipientLoader(context, CRYPTO_PROVIDER, QUERYSTRING);
|
||||
|
@ -225,6 +230,20 @@ public class RecipientLoaderTest extends RobolectricTest {
|
|||
assertEquals(RecipientCryptoStatus.UNAVAILABLE, recipients.get(0).getCryptoStatus());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void queryContactProviderWithoutPermission() throws Exception {
|
||||
shadowApp.denyPermissions(Manifest.permission.READ_CONTACTS);
|
||||
shadowApp.denyPermissions(Manifest.permission.WRITE_CONTACTS);
|
||||
|
||||
RecipientLoader recipientLoader = new RecipientLoader(context, CRYPTO_PROVIDER, QUERYSTRING);
|
||||
setupContactProvider("%" + QUERYSTRING + "%", CONTACT_1);
|
||||
|
||||
List<Recipient> recipients = recipientLoader.loadInBackground();
|
||||
|
||||
assertEquals(0, recipients.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryContactProvider_ignoresRecipientWithNoEmail() throws Exception {
|
||||
RecipientLoader recipientLoader = new RecipientLoader(context, CRYPTO_PROVIDER, QUERYSTRING);
|
||||
|
|
Loading…
Reference in a new issue