Skip to content

Commit

Permalink
Support Run-time permissions (for Contacts only, see below)
Browse files Browse the repository at this point in the history
The approach was to make it so that any K9Activity can easily request
whatever permission in the future.

The Contacts permission is now requested in two locations:

1.  When a list of Messages is displayed
2.  When a new message is first started to be composed.

The permission request is displayed ONCE per onCreate(). Any more than this
and it got really annoying.  A typical user who reads or writes emails
WILL see the request, trust me.  Once they see the message 2x, they
also have the option to block the requests from appearing.

If they DECLINE the request (or decline + DENY any further attempts),
the app should continues to work, albeit without incorporating contact data
(thumbnails, autocomplete, etc.). Contacts may still be added to the
Contacts app, as this uses an Intent and does not require any permission.

Once the Read Contacts permission is enabled, the app immediately begins
to use it.

To add other permissions in the future (such as External Storage access),
the request can be made in a similar way and the permission request result
handled appropriately by just adding it to K9Activity (or overriding in
a particular Activity).

I did *not* implement this for external storage access, as 1. there is a PR
waiting (thunderbird#3118) and 2. I think that using the Storage Access Framework
is a much better way to approach that as suggested in Issue thunderbird#2844.

I noticed there are 3 additional custom permissions in the manifest having
to do with other apps controlling K9, but I don't think they're required by
k9 itself.  If so, they can be requested from any K9Activity.
  • Loading branch information
fat-tire committed Feb 10, 2018
1 parent f4e4545 commit d85d4c5
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 6 deletions.
25 changes: 25 additions & 0 deletions k9mail/src/main/java/com/fsck/k9/activity/K9Activity.java
@@ -1,9 +1,12 @@
package com.fsck.k9.activity;

import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.MotionEvent;
import android.widget.Toast;

import com.fsck.k9.R;
import com.fsck.k9.activity.K9ActivityCommon.K9ActivityMagic;
import com.fsck.k9.activity.misc.SwipeGestureDetector.OnSwipeGestureListener;

Expand All @@ -12,6 +15,8 @@ public abstract class K9Activity extends Activity implements K9ActivityMagic {

private K9ActivityCommon mBase;

public static final int PERMISSIONS_REQUEST_READ_CONTACTS = 1;
public static final int PERMISSIONS_REQUEST_WRITE_CONTACTS = 2;

@Override
public void onCreate(Bundle savedInstanceState) {
Expand All @@ -29,4 +34,24 @@ public boolean dispatchTouchEvent(MotionEvent event) {
public void setupGestureDetector(OnSwipeGestureListener listener) {
mBase.setupGestureDetector(listener);
}

@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case PERMISSIONS_REQUEST_READ_CONTACTS:
case PERMISSIONS_REQUEST_WRITE_CONTACTS: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {

Toast.makeText(this, R.string.contact_permission_thanks,
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, R.string.contact_permission_request,
Toast.LENGTH_LONG).show();
}
}
}
}
}
18 changes: 15 additions & 3 deletions k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java
@@ -1,14 +1,13 @@
package com.fsck.k9.activity;


import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.app.AlertDialog;
Expand All @@ -21,13 +20,16 @@
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.TypedValue;
Expand Down Expand Up @@ -222,7 +224,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

checkPerms();
if (UpgradeDatabases.actionUpgradeDatabases(this, getIntent())) {
finish();
return;
Expand Down Expand Up @@ -644,6 +646,16 @@ protected void onRestoreInstanceState(Bundle savedInstanceState) {
updateMessageFormat();
}

private void checkPerms() {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_CONTACTS},
K9Activity.PERMISSIONS_REQUEST_READ_CONTACTS);
}
}

private void setTitle() {
setTitle(action.getTitleResource());
}
Expand Down
16 changes: 16 additions & 0 deletions k9mail/src/main/java/com/fsck/k9/activity/MessageList.java
@@ -1,6 +1,7 @@
package com.fsck.k9.activity;


import android.Manifest;
import java.util.Collection;
import java.util.List;

Expand All @@ -14,12 +15,15 @@
import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import timber.log.Timber;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
Expand Down Expand Up @@ -235,6 +239,7 @@ public void onCreate(Bundle savedInstanceState) {
if (cl.isFirstRun()) {
cl.getLogDialog().show();
}
checkPerms();
}

@Override
Expand Down Expand Up @@ -500,6 +505,17 @@ public void onPause() {
StorageManager.getInstance(getApplication()).removeListener(mStorageListener);
}


private void checkPerms() {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_CONTACTS},
K9Activity.PERMISSIONS_REQUEST_READ_CONTACTS);
}
}

@Override
public void onResume() {
super.onResume();
Expand Down
Expand Up @@ -5,17 +5,20 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.Manifest;
import android.content.AsyncTaskLoader;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.AbstractCursor;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Contacts.Data;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;

import com.fsck.k9.R;
import com.fsck.k9.mail.Address;
Expand Down Expand Up @@ -238,10 +241,68 @@ private static String getContactIdFromContactUri(Uri contactUri) {
}


private Boolean hasPerm() {
return
((ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED)
&& ((ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED)));

}

private Cursor getNicknameCursor(String nickname) {
nickname = "%" + nickname + "%";

Uri queryUriForNickname = ContactsContract.Data.CONTENT_URI;
if (!hasPerm()) {
// return empty cursor
return new 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;
}
};
}

return contentResolver.query(queryUriForNickname,
PROJECTION_NICKNAME,
Expand Down Expand Up @@ -315,8 +376,10 @@ private boolean fillContactDataFromNameAndEmail(String query, List<Recipient> re
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 (hasPerm()) {
cursor = contentResolver.query(queryUri, PROJECTION, selection, selectionArgs, SORT_ORDER);
}
if (cursor == null) {
return false;
}
Expand Down
61 changes: 61 additions & 0 deletions k9mail/src/main/java/com/fsck/k9/helper/Contacts.java
@@ -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;

Expand Down Expand Up @@ -255,6 +259,14 @@ public Uri getPhotoUri(String address) {
}
}

private Boolean hasPerm() {
return
((ContextCompat.checkSelfPermission(mContext,
Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED)
&& ((ContextCompat.checkSelfPermission(mContext,
Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED)));
}

/**
* Return a {@link Cursor} instance that can be used to fetch information
* about the contact with the given email address.
Expand All @@ -265,6 +277,55 @@ public Uri getPhotoUri(String address) {
*/
private Cursor getContactByAddress(final String address) {
final Uri uri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, Uri.encode(address));
if (!hasPerm()) {
// return blank cursor
return new 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;
}
};
} else
return mContentResolver.query(
uri,
PROJECTION,
Expand Down
4 changes: 4 additions & 0 deletions k9mail/src/main/res/values/strings.xml
Expand Up @@ -1286,4 +1286,8 @@ Please submit bug reports, contribute new features and ask questions at
<!-- notification channel(s) -->
<string name="k9_channel_id" translatable="false">k9_channel</string>
<string name="k9_channel_description">All notifications</string>

<!-- permissions -->
<string name="contact_permission_request">K-9 works best when this permission is granted.</string>
<string name="contact_permission_thanks">Your contact data is now available.</string>
</resources>

0 comments on commit d85d4c5

Please sign in to comment.