diff --git a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java b/src/main/java/eu/siacs/conversations/android/JabberIdContact.java index 1cb4dfa71..32d5a53e7 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 47cb567d7..8f6c3c1f2 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 ec6d93cec..584237156 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 6c4134b9f..394331452 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 18c1fb892..8b94dd440 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 f6849e6d1..98924a262 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; + 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); + } } - if (cursor == null) { - return null; - } - 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 0f8a16b46..3ee875fcf 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