make contact integration depend on manifest, not flavor

This commit is contained in:
Daniel Gultsch 2024-02-19 10:12:52 +01:00
parent 9b832e1285
commit 8e73b7f477
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
7 changed files with 138 additions and 57 deletions

View file

@ -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)) {

View file

@ -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() {

View file

@ -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)

View file

@ -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();

View file

@ -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)) {

View file

@ -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() {

View file

@ -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>