make contact integration depend on manifest, not flavor

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/android/JabberIdContact.java                    |   4 
src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java |  39 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java             |   2 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java                  |   6 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java               | 110 
src/main/java/eu/siacs/conversations/utils/PhoneHelper.java                          |  28 
src/main/res/values/strings.xml                                                      |   6 
7 files changed, 138 insertions(+), 57 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/android/JabberIdContact.java πŸ”—

@@ -62,8 +62,8 @@ public class JabberIdContact extends AbstractPhoneContact {
         return jid;
     }
 
-    public static Map<Jid, JabberIdContact> load(Context context) {
-        if (!QuickConversationsService.isFreeOrQuicksyFlavor()
+    public static Map<Jid, JabberIdContact> load(final Context context) {
+        if (!QuickConversationsService.isContactListIntegration(context)
                 || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
                         && context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
                                 != PackageManager.PERMISSION_GRANTED)) {

src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java πŸ”—

@@ -1,14 +1,22 @@
 package eu.siacs.conversations.services;
 
+import android.Manifest;
+import android.content.Context;
 import android.content.Intent;
-import android.os.Build;
+import android.content.pm.PackageManager;
+
+import com.google.common.collect.Iterables;
 
 import eu.siacs.conversations.BuildConfig;
 
+import java.util.Arrays;
+
 public abstract class AbstractQuickConversationsService {
 
+    public static final String SMS_RETRIEVED_ACTION =
+            "com.google.android.gms.auth.api.phone.SMS_RETRIEVED";
 
-    public static final String SMS_RETRIEVED_ACTION = "com.google.android.gms.auth.api.phone.SMS_RETRIEVED";
+    private static Boolean declaredReadContacts = null;
 
     protected final XmppConnectionService service;
 
@@ -30,8 +38,31 @@ public abstract class AbstractQuickConversationsService {
         return "playstore".equals(BuildConfig.FLAVOR_distribution);
     }
 
-    public static boolean isFreeOrQuicksyFlavor() {
-        return  "free".equals(BuildConfig.FLAVOR_distribution) || "quicksy".equals(BuildConfig.FLAVOR_mode);
+    public static boolean isContactListIntegration(final Context context) {
+        if ("quicksy".equals(BuildConfig.FLAVOR_mode)) {
+            return true;
+        }
+        final var readContacts = AbstractQuickConversationsService.declaredReadContacts;
+        if (readContacts != null) {
+            return Boolean.TRUE.equals(readContacts);
+        }
+        AbstractQuickConversationsService.declaredReadContacts = hasDeclaredReadContacts(context);
+        return AbstractQuickConversationsService.declaredReadContacts;
+    }
+
+    private static boolean hasDeclaredReadContacts(final Context context) {
+        final String[] permissions;
+        try {
+            permissions =
+                    context.getPackageManager()
+                            .getPackageInfo(
+                                    context.getPackageName(), PackageManager.GET_PERMISSIONS)
+                            .requestedPermissions;
+        } catch (final PackageManager.NameNotFoundException e) {
+            return false;
+        }
+        return Iterables.any(
+                Arrays.asList(permissions), p -> p.equals(Manifest.permission.READ_CONTACTS));
     }
 
     public static boolean isQuicksyPlayStore() {

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java πŸ”—

@@ -1290,7 +1290,7 @@ public class XmppConnectionService extends Service {
 
         restoreFromDatabase();
 
-        if (QuickConversationsService.isFreeOrQuicksyFlavor()
+        if (QuickConversationsService.isContactListIntegration(this)
                 && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
                         || ContextCompat.checkSelfPermission(
                                         this, Manifest.permission.READ_CONTACTS)

src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java πŸ”—

@@ -120,13 +120,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
     private void checkContactPermissionAndShowAddDialog() {
         if (hasContactsPermission()) {
             showAddToPhoneBookDialog();
-        } else if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        } else if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
         }
     }
 
     private boolean hasContactsPermission() {
-        if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
         } else {
             return true;
@@ -525,7 +525,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
     }
 
     private void onBadgeClick(final View view) {
-        if (QuickConversationsService.isFreeOrQuicksyFlavor()) {
+        if (QuickConversationsService.isContactListIntegration(this)) {
             final Uri systemAccount = contact.getSystemAccount();
             if (systemAccount == null) {
                 checkContactPermissionAndShowAddDialog();

src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java πŸ”—

@@ -6,12 +6,14 @@ import android.app.Dialog;
 import android.app.PendingIntent;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
+import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.preference.PreferenceManager;
 import android.text.Editable;
 import android.text.Html;
 import android.text.TextWatcher;
@@ -91,6 +93,8 @@ import eu.siacs.conversations.xmpp.XmppConnection;
 
 public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener {
 
+    private static final String PREF_KEY_CONTACT_INTEGRATION_CONSENT = "contact_list_integration_consent";
+
     public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri";
 
     private final int REQUEST_SYNC_CONTACTS = 0x28cf;
@@ -761,50 +765,96 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     private void askForContactsPermissions() {
-        if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+        if (QuickConversationsService.isContactListIntegration(this)
+                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
+                    != PackageManager.PERMISSION_GRANTED) {
                 if (mRequestedContactsPermission.compareAndSet(false, true)) {
-                    if (QuickConversationsService.isQuicksy() || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
+                    final String consent =
+                            PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
+                                    .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null);
+                    final boolean requiresConsent =
+                            (QuickConversationsService.isQuicksy()
+                                            || QuickConversationsService.isPlayStoreFlavor())
+                                    && !"agreed".equals(consent);
+                    if (requiresConsent && "declined".equals(consent)) {
+                        Log.d(Config.LOGTAG,"not asking for contacts permission because consent has been declined");
+                        return;
+                    }
+                    if (requiresConsent
+                            || shouldShowRequestPermissionRationale(
+                                    Manifest.permission.READ_CONTACTS)) {
                         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
                         final AtomicBoolean requestPermission = new AtomicBoolean(false);
                         if (QuickConversationsService.isQuicksy()) {
                             builder.setTitle(R.string.quicksy_wants_your_consent);
-                            builder.setMessage(Html.fromHtml(getString(R.string.sync_with_contacts_quicksy_static)));
+                            builder.setMessage(
+                                    Html.fromHtml(
+                                            getString(R.string.sync_with_contacts_quicksy_static)));
                         } else {
                             builder.setTitle(R.string.sync_with_contacts);
-                            builder.setMessage(getString(R.string.sync_with_contacts_long, getString(R.string.app_name)));
+                            builder.setMessage(
+                                    getString(
+                                            R.string.sync_with_contacts_long,
+                                            getString(R.string.app_name)));
                         }
                         @StringRes int confirmButtonText;
-                        if (QuickConversationsService.isConversations()) {
-                            confirmButtonText = R.string.next;
-                        } else {
+                        if (requiresConsent) {
                             confirmButtonText = R.string.agree_and_continue;
+                        } else {
+                            confirmButtonText = R.string.next;
                         }
-                        builder.setPositiveButton(confirmButtonText, (dialog, which) -> {
-                            if (requestPermission.compareAndSet(false, true)) {
-                                requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
-                            }
-                        });
-                        builder.setOnDismissListener(dialog -> {
-                            if (QuickConversationsService.isConversations() && requestPermission.compareAndSet(false, true)) {
-                                requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
-                            }
-                        });
-                        if (QuickConversationsService.isQuicksy()) {
-                            builder.setNegativeButton(R.string.decline, null);
+                        builder.setPositiveButton(
+                                confirmButtonText,
+                                (dialog, which) -> {
+                                    if (requiresConsent) {
+                                        PreferenceManager.getDefaultSharedPreferences(
+                                                        getApplicationContext())
+                                                .edit()
+                                                .putString(
+                                                        PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed")
+                                                .apply();
+                                    }
+                                    if (requestPermission.compareAndSet(false, true)) {
+                                        requestPermissions(
+                                                new String[] {Manifest.permission.READ_CONTACTS},
+                                                REQUEST_SYNC_CONTACTS);
+                                    }
+                                });
+                        if (requiresConsent) {
+                            builder.setNegativeButton(R.string.decline, (dialog, which) -> PreferenceManager.getDefaultSharedPreferences(
+                                            getApplicationContext())
+                                    .edit()
+                                    .putString(
+                                            PREF_KEY_CONTACT_INTEGRATION_CONSENT, "declined")
+                                    .apply());
+                        } else {
+                            builder.setOnDismissListener(
+                                    dialog -> {
+                                        if (requestPermission.compareAndSet(false, true)) {
+                                            requestPermissions(
+                                                    new String[] {
+                                                        Manifest.permission.READ_CONTACTS
+                                                    },
+                                                    REQUEST_SYNC_CONTACTS);
+                                        }
+                                    });
                         }
-                        builder.setCancelable(QuickConversationsService.isQuicksy());
+                        builder.setCancelable(requiresConsent);
                         final AlertDialog dialog = builder.create();
-                        dialog.setCanceledOnTouchOutside(QuickConversationsService.isQuicksy());
-                        dialog.setOnShowListener(dialogInterface -> {
-                            final TextView tv = dialog.findViewById(android.R.id.message);
-                            if (tv != null) {
-                                tv.setMovementMethod(LinkMovementMethod.getInstance());
-                            }
-                        });
+                        dialog.setCanceledOnTouchOutside(requiresConsent);
+                        dialog.setOnShowListener(
+                                dialogInterface -> {
+                                    final TextView tv = dialog.findViewById(android.R.id.message);
+                                    if (tv != null) {
+                                        tv.setMovementMethod(LinkMovementMethod.getInstance());
+                                    }
+                                });
                         dialog.show();
                     } else {
-                        requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
+                        requestPermissions(
+                                new String[] {Manifest.permission.READ_CONTACTS},
+                                REQUEST_SYNC_CONTACTS);
                     }
                 }
             }
@@ -840,7 +890,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 
     @Override
     protected void onBackendConnected() {
-        if (QuickConversationsService.isFreeOrQuicksyFlavor()
+        if (QuickConversationsService.isContactListIntegration(this)
                 && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
                         || checkSelfPermission(Manifest.permission.READ_CONTACTS)
                                 == PackageManager.PERMISSION_GRANTED)) {

src/main/java/eu/siacs/conversations/utils/PhoneHelper.java πŸ”—

@@ -10,6 +10,8 @@ import android.os.Build;
 import android.provider.ContactsContract.Profile;
 import android.provider.Settings;
 
+import com.google.common.base.Strings;
+
 import eu.siacs.conversations.services.QuickConversationsService;
 
 public class PhoneHelper {
@@ -20,27 +22,25 @@ public class PhoneHelper {
     }
 
     public static Uri getProfilePictureUri(final Context context) {
-        if (!QuickConversationsService.isFreeOrQuicksyFlavor()
+        if (!QuickConversationsService.isContactListIntegration(context)
                 || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
                         && context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
                                 != PackageManager.PERMISSION_GRANTED)) {
             return null;
         }
         final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI};
-        final Cursor cursor;
-        try {
-            cursor =
-                    context.getContentResolver()
-                            .query(Profile.CONTENT_URI, projection, null, null, null);
-        } catch (Throwable e) {
-            return null;
-        }
-        if (cursor == null) {
-            return null;
+        try (final Cursor cursor =
+                context.getContentResolver()
+                        .query(Profile.CONTENT_URI, projection, null, null, null)) {
+            if (cursor != null && cursor.moveToFirst()) {
+                final var photoUri = cursor.getString(1);
+                if (Strings.isNullOrEmpty(photoUri)) {
+                    return null;
+                }
+                return Uri.parse(photoUri);
+            }
         }
-        final String uri = cursor.moveToFirst() ? cursor.getString(1) : null;
-        cursor.close();
-        return uri == null ? null : Uri.parse(uri);
+        return null;
     }
 
     public static boolean isEmulator() {

src/main/res/values/strings.xml πŸ”—

@@ -518,8 +518,8 @@
     <string name="no_storage_permission">Grant %1$s access to external storage</string>
     <string name="no_camera_permission">Grant %1$s access to the camera</string>
     <string name="quicksy_wants_your_consent">Quicksy asks for your consent to use your data</string>
-    <string name="sync_with_contacts">Synchronize with contacts</string>
-    <string name="sync_with_contacts_long">%1$s wants permission to access your address book to match it with your XMPP contact list.\nThis will display your contacts’ full names and avatars.\n\n%1$s will only read your address book and match it locally without uploading anything to your server.</string>
+    <string name="sync_with_contacts">Contact list integration</string>
+    <string name="sync_with_contacts_long">%1$s processes your contact list locally, on your device, to show you the names and profile pictures for matching contacts on XMPP.\n\nNo contact list data ever leaves your device!</string>
     <string name="sync_with_contacts_quicksy_static" translatable="false"><![CDATA[Quicksy syncs your contact list in regular intervals to make suggestions about possible contacts, who are already using the app, even when the app is closed or not in use.<br><br>Find more information in our <a href="https://quicksy.im/privacy.htm">Privacy Policy</a>.]]></string>
     <string name="notify_on_all_messages">Notify on all messages</string>
     <string name="notify_only_when_highlighted">Notify only when mentioned</string>
@@ -1024,5 +1024,5 @@
     <string name="report_spam">Report spam</string>
     <string name="report_spam_and_block">Report spam and block spammer</string>
     <string name="privacy_policy">Privacy policy</string>
-    <string name="contact_list_integration_not_available">Address book integration is not available</string>
+    <string name="contact_list_integration_not_available">Contact list integration is not available</string>
 </resources>