From 8e73b7f47756ff4191b101ded543e0eb4ea114ad Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 19 Feb 2024 10:12:52 +0100 Subject: [PATCH] make contact integration depend on manifest, not flavor --- .../android/JabberIdContact.java | 4 +- .../AbstractQuickConversationsService.java | 39 ++++++- .../services/XmppConnectionService.java | 2 +- .../ui/ContactDetailsActivity.java | 6 +- .../ui/StartConversationActivity.java | 110 +++++++++++++----- .../conversations/utils/PhoneHelper.java | 28 ++--- src/main/res/values/strings.xml | 6 +- 7 files changed, 138 insertions(+), 57 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java b/src/main/java/eu/siacs/conversations/android/JabberIdContact.java index 1cb4dfa71b68162e0f640f97619c077b67f4775f..32d5a53e75e4397e56fa7cae611f9a4d7cc68d08 100644 --- a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java +++ b/src/main/java/eu/siacs/conversations/android/JabberIdContact.java @@ -62,8 +62,8 @@ public class JabberIdContact extends AbstractPhoneContact { return jid; } - public static Map load(Context context) { - if (!QuickConversationsService.isFreeOrQuicksyFlavor() + public static Map 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)) { diff --git a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java b/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java index 47cb567d7d5f86e8ad0ca9b6890db904a8537f18..8f6c3c1f2a1edf6c01d98a48b01ae8d888b45d0b 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java +++ b/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() { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ec6d93cec69825b866c07827b83b98df5d5d7021..58423715684bd1b42aa7a1dc63cf32b93d718b0d 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/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) diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 6c4134b9fab820d3b4ac7beb0ddd0de82815c237..394331452996cc099b74832d3e240dbf8c03e380 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/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(); diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 18c1fb892732a172bf586188edc0ed38d5a04b9a..8b94dd44039adf17a9e85c20e329581d81828f83 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/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)) { diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java index f6849e6d1ec351fae6093b44c3f568ea9913d6c5..98924a26214c2ec03a33f43c2987e08f155237ae 100644 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ b/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() { diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 0f8a16b468ee5e669d7e3bd3f32bea2cded81b91..3ee875fcf49c8cae6110f0728d627e76e1f045a7 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -518,8 +518,8 @@ Grant %1$s access to external storage Grant %1$s access to the camera Quicksy asks for your consent to use your data - Synchronize with contacts - %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. + Contact list integration + %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!
Find more information in our Privacy Policy.]]>
Notify on all messages Notify only when mentioned @@ -1024,5 +1024,5 @@ Report spam Report spam and block spammer Privacy policy - Address book integration is not available + Contact list integration is not available