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 up please make sure that WiFi is turned on, that Crosswords is
installed, and that play via WiFi Direct is enabled.</string> 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 <string name="warn_nomobile_fmt">The number %1$s for %2$s is not
a \"mobile\" number. Import anyway?</string> a \"mobile\" number. Import anyway?</string>
@ -2732,6 +2730,13 @@
safe to permanently deny permission. safe to permanently deny permission.
</string> </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="remove_sms">Remove SMS</string>
<string name="contact_not_found">Not in Contacts</string>
</resources> </resources>

View file

@ -1705,8 +1705,6 @@
pu esaelp ekam erus taht IfIw si denrut ,no taht Sdrowssorc si pu esaelp ekam erus taht IfIw si denrut ,no taht Sdrowssorc si
,dellatsni dna taht yalp aiv IfIw Tcerid si delbane.</string> ,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 <string name="warn_nomobile_fmt">Eht rebmun %1$s rof %2$s si ton
a \"elibom\" rebmun. Tropmi ?yawyna</string> a \"elibom\" rebmun. Tropmi ?yawyna</string>
<!-- Shows in SMS Invite dialog when no phone numbers have been saved previously --> <!-- 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 e.g. esuaceb uoy yap rof hcae egassem ro evah a Nozirev ,enohp ti\'s
efas ot yltnenamrep yned noissimrep. efas ot yltnenamrep yned noissimrep.
</string> </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="remove_sms">Evomer SMS</string>
<string name="contact_not_found">Ton ni Stcatnoc</string>
</resources> </resources>

View file

@ -1705,8 +1705,6 @@
UP PLEASE MAKE SURE THAT WIFI IS TURNED ON, THAT CROSSWORDS IS UP PLEASE MAKE SURE THAT WIFI IS TURNED ON, THAT CROSSWORDS IS
INSTALLED, AND THAT PLAY VIA WIFI DIRECT IS ENABLED.</string> 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 <string name="warn_nomobile_fmt">THE NUMBER %1$s FOR %2$s IS NOT
A \"MOBILE\" NUMBER. IMPORT ANYWAY?</string> A \"MOBILE\" NUMBER. IMPORT ANYWAY?</string>
<!-- Shows in SMS Invite dialog when no phone numbers have been saved previously --> <!-- 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 E.G. BECAUSE YOU PAY FOR EACH MESSAGE OR HAVE A VERIZON PHONE, IT\'S
SAFE TO PERMANENTLY DENY PERMISSION. SAFE TO PERMANENTLY DENY PERMISSION.
</string> </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="remove_sms">REMOVE SMS</string>
<string name="contact_not_found">NOT IN CONTACTS</string>
</resources> </resources>

View file

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

View file

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

View file

@ -48,8 +48,12 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map;
import java.util.Set; import java.util.Set;
import org.json.JSONObject;
import org.json.JSONException;
public class SMSInviteDelegate extends InviteDelegate public class SMSInviteDelegate extends InviteDelegate
implements View.OnClickListener { implements View.OnClickListener {
private static final String TAG = SMSInviteDelegate.class.getSimpleName(); private static final String TAG = SMSInviteDelegate.class.getSimpleName();
@ -100,6 +104,8 @@ public class SMSInviteDelegate extends InviteDelegate
getSavedState(); getSavedState();
rebuildList( true ); rebuildList( true );
askContactsPermission( true );
} }
@Override @Override
@ -210,33 +216,49 @@ public class SMSInviteDelegate extends InviteDelegate
// DlgDelegate.DlgClickNotify interface // DlgDelegate.DlgClickNotify interface
@Override @Override
public void dlgButtonClicked( Action action, int which, Object[] params ) public void dlgButtonClicked( Action action, int which,
final Object[] params )
{ {
switch( which ) { boolean isPositive = AlertDialog.BUTTON_POSITIVE == which;
case AlertDialog.BUTTON_POSITIVE: DbgUtils.logd( TAG, "dlgButtonClicked(%s,pos:%b)",
switch( action ) { action.toString(), isPositive );
case CLEAR_ACTION:
clearSelectedImpl(); switch ( action ) {
case RETRY_CONTACTS_ACTION:
askContactsPermission( false );
break; break;
case USE_IMMOBILE_ACTION: case CLEAR_ACTION:
m_immobileConfirmed = true; if ( isPositive) {
clearSelectedImpl();
}
break; break;
case POST_WARNING_ACTION: case POST_WARNING_ACTION:
DbgUtils.printStack( TAG );
if ( isPositive ) { // ???
Assert.assertTrue( ((String)params[0]).equals(m_pendingNumber) ); Assert.assertTrue( ((String)params[0]).equals(m_pendingNumber) );
Assert.assertTrue( params[1] == null Assert.assertTrue( params[1] == null
|| ((String)params[1]).equals(m_pendingName) ); || ((String)params[1]).equals(m_pendingName) );
m_phoneRecs.add( new PhoneRec( m_pendingName, m_pendingNumber ) ); m_phoneRecs.add( new PhoneRec( m_pendingName, m_pendingNumber ) );
saveAndRebuild(); saveAndRebuild();
break;
} }
break; break;
case DlgDelegate.DISMISS_BUTTON: case USE_IMMOBILE_ACTION:
if ( Action.USE_IMMOBILE_ACTION == action && m_immobileConfirmed ) { 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, makeConfirmThenBuilder( R.string.warn_unlimited,
Action.POST_WARNING_ACTION ) Action.POST_WARNING_ACTION )
.setPosButton( R.string.button_yes ) .setPosButton( R.string.button_yes )
.setParams( params )
.show(); .show();
} }
} );
}
break; break;
} }
} }
@ -279,6 +301,7 @@ public class SMSInviteDelegate extends InviteDelegate
number, name ); number, name );
makeConfirmThenBuilder( msg, Action.USE_IMMOBILE_ACTION ) makeConfirmThenBuilder( msg, Action.USE_IMMOBILE_ACTION )
.setPosButton( R.string.button_yes ) .setPosButton( R.string.button_yes )
.setParams( number, name )
.show(); .show();
} }
} }
@ -306,22 +329,28 @@ public class SMSInviteDelegate extends InviteDelegate
private void getSavedState() private void getSavedState()
{ {
String[] phones = XWPrefs.getSMSPhones( m_activity ); JSONObject phones = XWPrefs.getSMSPhones( m_activity );
m_phoneRecs = new ArrayList<PhoneRec>(phones.length); m_phoneRecs = new ArrayList<PhoneRec>();
for ( String phone : phones ) { for ( Iterator<String> iter = phones.keys(); iter.hasNext(); ) {
PhoneRec rec = new PhoneRec( null, phone ); String phone = iter.next();
String name = phones.optString( phone, null );
PhoneRec rec = new PhoneRec( name, phone );
m_phoneRecs.add( rec ); m_phoneRecs.add( rec );
} }
} }
private void saveAndRebuild() private void saveAndRebuild()
{ {
String[] phones = new String[m_phoneRecs.size()]; JSONObject phones = new JSONObject();
Iterator<PhoneRec> iter = m_phoneRecs.iterator(); Iterator<PhoneRec> iter = m_phoneRecs.iterator();
for ( int ii = 0; iter.hasNext(); ++ii ) { while ( iter.hasNext() ) {
PhoneRec rec = iter.next(); 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 ); XWPrefs.setSMSPhones( m_activity, phones );
@ -340,6 +369,23 @@ public class SMSInviteDelegate extends InviteDelegate
saveAndRebuild(); 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 { private class PhoneRec implements InviterItem {
public String m_phone; public String m_phone;
public String m_name; public String m_name;
@ -354,10 +400,7 @@ public class SMSInviteDelegate extends InviteDelegate
m_phone = phone; m_phone = phone;
if ( null == name ) { if ( null == name ) {
name = Utils.phoneToContact( m_activity, phone, false ); name = getString( R.string.contact_not_found );
if ( null == name ) {
name = getString( R.string.manual_owner_name );
}
} }
m_name = name; m_name = name;
} }

View file

@ -295,7 +295,8 @@ public class Utils {
cursor.close(); cursor.close();
s_phonesHash.put( phone, name ); s_phonesHash.put( phone, name );
} catch ( Exception ex ) { } 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.preference.PreferenceManager;
import android.text.TextUtils; import android.text.TextUtils;
import org.json.JSONException;
import org.json.JSONObject;
import junit.framework.Assert; import junit.framework.Assert;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet;
public class XWPrefs { 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! // No reason to put this in xml if they're private to this file!
private static final String key_checked_upgrades = "key_checked_upgrades"; 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 ); 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 // Used by RelayInviteDelegate.java