permissions: ask for READ_CONTACTS

On launching SMS invite dialog, ask for READ_CONTACTS permission. Where
it's needed, though, is for the user-invisible process of translating
phone numbers to names (not launching the Contacts app via an
Intent). This just seems like the best place to ask since the user's
thinking about contacts and phone numbers.
This commit is contained in:
Eric House 2016-12-19 06:34:20 -08:00
parent 09188bc704
commit f199aef738
9 changed files with 474 additions and 378 deletions

File diff suppressed because it is too large Load diff

View file

@ -1971,8 +1971,6 @@
up please make sure that WiFi is turned on, that Crosswords is
installed, and that play via WiFi Direct is enabled.</string>
<!-- -->
<string name="manual_owner_name">(Not in contacts)</string>
<!-- -->
<string name="warn_nomobile_fmt">The number %1$s for %2$s is not
a \"mobile\" number. Import anyway?</string>
@ -2732,6 +2730,13 @@
safe to permanently deny permission.
</string>
<string name="contacts_rationale">Crosswords want access to your
contacts in order to put a name to phone numbers that send you
invitations via SMS. You\'ll still be able to receive invitations if
you don\'t grant this permission, but only the phone number of the
sender will be displayed.</string>
<string name="remove_sms">Remove SMS</string>
<string name="contact_not_found">Not in Contacts</string>
</resources>

View file

@ -1705,8 +1705,6 @@
pu esaelp ekam erus taht IfIw si denrut ,no taht Sdrowssorc si
,dellatsni dna taht yalp aiv IfIw Tcerid si delbane.</string>
<!-- -->
<string name="manual_owner_name">tOn( ni )stcatnoc</string>
<!-- -->
<string name="warn_nomobile_fmt">Eht rebmun %1$s rof %2$s si ton
a \"elibom\" rebmun. Tropmi ?yawyna</string>
<!-- Shows in SMS Invite dialog when no phone numbers have been saved previously -->
@ -2332,5 +2330,11 @@
e.g. esuaceb uoy yap rof hcae egassem ro evah a Nozirev ,enohp ti\'s
efas ot yltnenamrep yned noissimrep.
</string>
<string name="contacts_rationale">Sdrowssorc tnaw ssecca ot ruoy
stcatnoc ni redro ot tup a eman ot enohp srebmun taht dnes uoy
snoitativni aiv SMS. Uoy\'ll llits eb elba ot eviecer snoitativni fi
uoy nod\'t tnarg siht ,noissimrep tub ylno eht enohp rebmun fo eht
rednes lliw eb deyalpsid.</string>
<string name="remove_sms">Evomer SMS</string>
<string name="contact_not_found">Ton ni Stcatnoc</string>
</resources>

View file

@ -1705,8 +1705,6 @@
UP PLEASE MAKE SURE THAT WIFI IS TURNED ON, THAT CROSSWORDS IS
INSTALLED, AND THAT PLAY VIA WIFI DIRECT IS ENABLED.</string>
<!-- -->
<string name="manual_owner_name">(NOT IN CONTACTS)</string>
<!-- -->
<string name="warn_nomobile_fmt">THE NUMBER %1$s FOR %2$s IS NOT
A \"MOBILE\" NUMBER. IMPORT ANYWAY?</string>
<!-- Shows in SMS Invite dialog when no phone numbers have been saved previously -->
@ -2332,5 +2330,11 @@
E.G. BECAUSE YOU PAY FOR EACH MESSAGE OR HAVE A VERIZON PHONE, IT\'S
SAFE TO PERMANENTLY DENY PERMISSION.
</string>
<string name="contacts_rationale">CROSSWORDS WANT ACCESS TO YOUR
CONTACTS IN ORDER TO PUT A NAME TO PHONE NUMBERS THAT SEND YOU
INVITATIONS VIA SMS. YOU\'LL STILL BE ABLE TO RECEIVE INVITATIONS IF
YOU DON\'T GRANT THIS PERMISSION, BUT ONLY THE PHONE NUMBER OF THE
SENDER WILL BE DISPLAYED.</string>
<string name="remove_sms">REMOVE SMS</string>
<string name="contact_not_found">NOT IN CONTACTS</string>
</resources>

View file

@ -112,6 +112,7 @@ public class DlgDelegate {
CLEAR_ACTION,
USE_IMMOBILE_ACTION,
POST_WARNING_ACTION,
RETRY_CONTACTS_ACTION,
// BT Invite
OPEN_BT_PREFS_ACTION,

View file

@ -87,6 +87,11 @@ public class Perms23 {
return this;
}
public void asyncQuery( Activity activity )
{
asyncQuery( activity, null );
}
public void asyncQuery( Activity activity, PermCbck cbck )
{
DbgUtils.logd( TAG, "asyncQuery(%s)", m_perms.toString() );
@ -117,11 +122,13 @@ public class Perms23 {
if ( 0 < needShow.size() && null != m_onShow ) {
m_onShow.onShouldShowRationale( needShow );
} else if ( haveAll ) {
Map<Perm, Boolean> map = new HashMap<Perm, Boolean>();
for ( Perm perm : m_perms ) {
map.put( perm, true );
if ( null != cbck ) {
Map<Perm, Boolean> map = new HashMap<Perm, Boolean>();
for ( Perm perm : m_perms ) {
map.put( perm, true );
}
cbck.onPermissionResult( map );
}
cbck.onPermissionResult( map );
} else {
String[] permsArray = askStrings.toArray( new String[askStrings.size()] );
int code = register( cbck );
@ -135,16 +142,19 @@ public class Perms23 {
private static Map<Integer, PermCbck> s_map = new HashMap<Integer, PermCbck>();
public static void gotPermissionResult( int code, String[] perms, int[] granteds )
{
Map<Perm, Boolean> result = new HashMap<Perm, Boolean>();
for ( int ii = 0; ii < perms.length; ++ii ) {
Perm perm = Perm.getFor( perms[ii] );
boolean granted = PackageManager.PERMISSION_GRANTED == granteds[ii];
result.put( perm, granted );
// DbgUtils.logd( TAG, "calling %s.onPermissionResult(%s, %b)",
// record.cbck.getClass().getSimpleName(), perm.toString(),
// granted );
PermCbck cbck = s_map.get( code );
if ( null != cbck ) {
Map<Perm, Boolean> result = new HashMap<Perm, Boolean>();
for ( int ii = 0; ii < perms.length; ++ii ) {
Perm perm = Perm.getFor( perms[ii] );
boolean granted = PackageManager.PERMISSION_GRANTED == granteds[ii];
result.put( perm, granted );
// DbgUtils.logd( TAG, "calling %s.onPermissionResult(%s, %b)",
// record.cbck.getClass().getSimpleName(), perm.toString(),
// granted );
}
cbck.onPermissionResult( result );
}
s_map.get( code ).onPermissionResult( result );
}
public static boolean havePermission( Perm perm )

View file

@ -48,8 +48,12 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.json.JSONObject;
import org.json.JSONException;
public class SMSInviteDelegate extends InviteDelegate
implements View.OnClickListener {
private static final String TAG = SMSInviteDelegate.class.getSimpleName();
@ -100,6 +104,8 @@ public class SMSInviteDelegate extends InviteDelegate
getSavedState();
rebuildList( true );
askContactsPermission( true );
}
@Override
@ -210,32 +216,48 @@ public class SMSInviteDelegate extends InviteDelegate
// DlgDelegate.DlgClickNotify interface
@Override
public void dlgButtonClicked( Action action, int which, Object[] params )
public void dlgButtonClicked( Action action, int which,
final Object[] params )
{
switch( which ) {
case AlertDialog.BUTTON_POSITIVE:
switch( action ) {
case CLEAR_ACTION:
boolean isPositive = AlertDialog.BUTTON_POSITIVE == which;
DbgUtils.logd( TAG, "dlgButtonClicked(%s,pos:%b)",
action.toString(), isPositive );
switch ( action ) {
case RETRY_CONTACTS_ACTION:
askContactsPermission( false );
break;
case CLEAR_ACTION:
if ( isPositive) {
clearSelectedImpl();
break;
case USE_IMMOBILE_ACTION:
m_immobileConfirmed = true;
break;
case POST_WARNING_ACTION:
}
break;
case POST_WARNING_ACTION:
DbgUtils.printStack( TAG );
if ( isPositive ) { // ???
Assert.assertTrue( ((String)params[0]).equals(m_pendingNumber) );
Assert.assertTrue( params[1] == null
|| ((String)params[1]).equals(m_pendingName) );
m_phoneRecs.add( new PhoneRec( m_pendingName, m_pendingNumber ) );
saveAndRebuild();
break;
}
break;
case DlgDelegate.DISMISS_BUTTON:
if ( Action.USE_IMMOBILE_ACTION == action && m_immobileConfirmed ) {
makeConfirmThenBuilder( R.string.warn_unlimited,
Action.POST_WARNING_ACTION )
.setPosButton( R.string.button_yes )
.show();
case USE_IMMOBILE_ACTION:
if ( isPositive ) {
m_immobileConfirmed = true;
} else if ( m_immobileConfirmed ) {
// Putting up a new alert from inside another's handler
// confuses things. So post instead.
post( new Runnable() {
@Override
public void run() {
makeConfirmThenBuilder( R.string.warn_unlimited,
Action.POST_WARNING_ACTION )
.setPosButton( R.string.button_yes )
.setParams( params )
.show();
}
} );
}
break;
}
@ -279,6 +301,7 @@ public class SMSInviteDelegate extends InviteDelegate
number, name );
makeConfirmThenBuilder( msg, Action.USE_IMMOBILE_ACTION )
.setPosButton( R.string.button_yes )
.setParams( number, name )
.show();
}
}
@ -306,22 +329,28 @@ public class SMSInviteDelegate extends InviteDelegate
private void getSavedState()
{
String[] phones = XWPrefs.getSMSPhones( m_activity );
JSONObject phones = XWPrefs.getSMSPhones( m_activity );
m_phoneRecs = new ArrayList<PhoneRec>(phones.length);
for ( String phone : phones ) {
PhoneRec rec = new PhoneRec( null, phone );
m_phoneRecs = new ArrayList<PhoneRec>();
for ( Iterator<String> iter = phones.keys(); iter.hasNext(); ) {
String phone = iter.next();
String name = phones.optString( phone, null );
PhoneRec rec = new PhoneRec( name, phone );
m_phoneRecs.add( rec );
}
}
private void saveAndRebuild()
{
String[] phones = new String[m_phoneRecs.size()];
JSONObject phones = new JSONObject();
Iterator<PhoneRec> iter = m_phoneRecs.iterator();
for ( int ii = 0; iter.hasNext(); ++ii ) {
while ( iter.hasNext() ) {
PhoneRec rec = iter.next();
phones[ii] = rec.m_phone;
try {
phones.put( rec.m_phone, rec.m_name );
} catch ( JSONException ex ) {
DbgUtils.logex( TAG, ex );
}
}
XWPrefs.setSMSPhones( m_activity, phones );
@ -340,6 +369,23 @@ public class SMSInviteDelegate extends InviteDelegate
saveAndRebuild();
}
private void askContactsPermission( boolean showRationale )
{
Perms23.Builder builder = new Perms23.Builder( Perms23.Perm.READ_CONTACTS );
if ( showRationale ) {
builder.setOnShowRationale( new Perms23.OnShowRationale() {
@Override
public void onShouldShowRationale( Set<Perms23.Perm> perms )
{
makeOkOnlyBuilder( R.string.contacts_rationale )
.setAction( Action.RETRY_CONTACTS_ACTION )
.show();
}
} );
}
builder.asyncQuery( m_activity );
}
private class PhoneRec implements InviterItem {
public String m_phone;
public String m_name;
@ -354,10 +400,7 @@ public class SMSInviteDelegate extends InviteDelegate
m_phone = phone;
if ( null == name ) {
name = Utils.phoneToContact( m_activity, phone, false );
if ( null == name ) {
name = getString( R.string.manual_owner_name );
}
name = getString( R.string.contact_not_found );
}
m_name = name;
}

View file

@ -295,7 +295,8 @@ public class Utils {
cursor.close();
s_phonesHash.put( phone, name );
} catch ( Exception ex ) {
name = "not found";
// could just be lack of permsisions
name = null;
}
}
}

View file

@ -26,12 +26,16 @@ import android.content.res.Configuration;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import org.json.JSONException;
import org.json.JSONObject;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet;
public class XWPrefs {
private static final String TAG = XWPrefs.class.getSimpleName();
// No reason to put this in xml if they're private to this file!
private static final String key_checked_upgrades = "key_checked_upgrades";
@ -236,14 +240,39 @@ public class XWPrefs {
return getPrefsStringArray( context, R.string.key_closed_langs );
}
public static void setSMSPhones( Context context, String[] names )
public static void setSMSPhones( Context context, JSONObject phones )
{
setPrefsStringArray( context, R.string.key_sms_phones, names );
setPrefsString( context, R.string.key_sms_phones, phones.toString() );
}
public static String[] getSMSPhones( Context context )
public static JSONObject getSMSPhones( Context context )
{
return getPrefsStringArray( context, R.string.key_sms_phones );
String asStr = getPrefsString( context, R.string.key_sms_phones );
JSONObject obj = null;
if ( null != asStr ) {
try {
obj = new JSONObject( asStr );
} catch ( JSONException ex ) {
obj = null;
}
}
if ( null == obj ) {
obj = new JSONObject();
if ( null != asStr ) {
String[] numbers = TextUtils.split( asStr, "\n" );
for ( String number : numbers ) {
try {
obj.put( number, (String)null );
} catch ( JSONException ex ) {
DbgUtils.logex( TAG, ex );
}
}
}
}
return obj;
}
// Used by RelayInviteDelegate.java