Added downloading Outlook contacts

This commit is contained in:
M66B 2023-06-12 14:34:20 +02:00
parent f07a4682dd
commit b8085a64f1
6 changed files with 266 additions and 0 deletions

View File

@ -92,6 +92,8 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
static final int REQUEST_CHANGE_PASSWORD = 10;
static final int REQUEST_DELETE_ACCOUNT = 11;
static final int REQUEST_IMPORT_PROVIDERS = 12;
static final int REQUEST_GRAPH_CONTACTS = 13;
static final int REQUEST_GRAPH_CONTACTS_OAUTH = 14;
static final int PI_CONNECTION = 1;
static final int PI_MISC = 2;

View File

@ -35,6 +35,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import java.util.ArrayList;
import java.util.List;
public class FragmentDialogSelectAccount extends FragmentDialogBase {
@ -81,6 +82,10 @@ public class FragmentDialogSelectAccount extends FragmentDialogBase {
@Override
protected void onExecuted(Bundle args, List<EntityAccount> accounts) {
if ("outlook".equals(args.getString("filter")))
for (EntityAccount account : new ArrayList<>(accounts))
if (!account.isOutlook())
accounts.remove(account);
adapter.addAll(accounts);
}
@ -101,6 +106,7 @@ public class FragmentDialogSelectAccount extends FragmentDialogBase {
args.putLong("account", account.id);
args.putInt("protocol", account.protocol);
args.putString("name", account.name);
args.putString("user", account.user);
sendResult(RESULT_OK);
}
})

View File

@ -54,6 +54,7 @@ import android.widget.CompoundButton;
import android.widget.ImageButton;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -70,7 +71,31 @@ import androidx.lifecycle.Observer;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.preference.PreferenceManager;
import net.openid.appauth.AppAuthConfiguration;
import net.openid.appauth.AuthState;
import net.openid.appauth.AuthorizationException;
import net.openid.appauth.AuthorizationRequest;
import net.openid.appauth.AuthorizationResponse;
import net.openid.appauth.AuthorizationService;
import net.openid.appauth.AuthorizationServiceConfiguration;
import net.openid.appauth.ClientAuthentication;
import net.openid.appauth.ClientSecretPost;
import net.openid.appauth.GrantTypeValues;
import net.openid.appauth.NoClientAuthentication;
import net.openid.appauth.ResponseTypeValues;
import net.openid.appauth.TokenRequest;
import net.openid.appauth.TokenResponse;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
public class FragmentSetup extends FragmentBase implements SharedPreferences.OnSharedPreferenceChangeListener {
@ -107,6 +132,7 @@ public class FragmentSetup extends FragmentBase implements SharedPreferences.OnS
private Button btnPermissions;
private TextView tvPermissionsWhy;
private TextView tvImportContacts;
private Button btnGraphContacts;
private TextView tvDozeDone;
private Button btnDoze;
@ -146,6 +172,8 @@ public class FragmentSetup extends FragmentBase implements SharedPreferences.OnS
private boolean manual = false;
private static final String GRAPH_SCOPE_READ_CONTACTS = "https://graph.microsoft.com/Contacts.Read";
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@ -195,6 +223,7 @@ public class FragmentSetup extends FragmentBase implements SharedPreferences.OnS
btnPermissions = view.findViewById(R.id.btnPermissions);
tvPermissionsWhy = view.findViewById(R.id.tvPermissionsWhy);
tvImportContacts = view.findViewById(R.id.tvImportContacts);
btnGraphContacts = view.findViewById(R.id.btnGraphContacts);
tvDozeDone = view.findViewById(R.id.tvDozeDone);
btnDoze = view.findViewById(R.id.btnDoze);
@ -564,6 +593,20 @@ public class FragmentSetup extends FragmentBase implements SharedPreferences.OnS
}
});
btnGraphContacts.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Bundle args = new Bundle();
args.putInt("type", EntityAccount.TYPE_IMAP);
args.putString("filter", "outlook");
FragmentDialogSelectAccount fragment = new FragmentDialogSelectAccount();
fragment.setArguments(args);
fragment.setTargetFragment(FragmentSetup.this, ActivitySetup.REQUEST_GRAPH_CONTACTS);
fragment.show(getParentFragmentManager(), "account:contacts");
}
});
btnDoze.setOnClickListener(new View.OnClickListener() {
@Override
@SuppressLint("BatteryLife")
@ -1073,6 +1116,14 @@ public class FragmentSetup extends FragmentBase implements SharedPreferences.OnS
if (resultCode == RESULT_OK && data != null)
onDeleteAccount(data.getBundleExtra("args"));
break;
case ActivitySetup.REQUEST_GRAPH_CONTACTS:
if (resultCode == RESULT_OK && data != null)
handleImportGraphContacts(data.getBundleExtra("args"));
break;
case ActivitySetup.REQUEST_GRAPH_CONTACTS_OAUTH:
if (resultCode == RESULT_OK && data != null)
onHandleGraphContactsOAuth(data);
break;
}
} catch (Throwable ex) {
Log.e(ex);
@ -1213,6 +1264,132 @@ public class FragmentSetup extends FragmentBase implements SharedPreferences.OnS
.show();
}
private void handleImportGraphContacts(Bundle args) {
try {
final Context context = getContext();
long account = args.getLong("account");
String user = args.getString("user");
EmailProvider provider = EmailProvider.getProvider(context, "outlookgraph");
AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder()
.build();
AuthorizationService authService = new AuthorizationService(context, appAuthConfig);
AuthorizationServiceConfiguration serviceConfig = new AuthorizationServiceConfiguration(
Uri.parse(provider.graph.authorizationEndpoint),
Uri.parse(provider.graph.tokenEndpoint));
AuthorizationRequest.Builder authRequestBuilder =
new AuthorizationRequest.Builder(
serviceConfig,
provider.graph.clientId,
ResponseTypeValues.CODE,
Uri.parse(provider.graph.redirectUri))
.setScopes(GRAPH_SCOPE_READ_CONTACTS)
.setState(provider.id + ":" + account)
.setLoginHint(user);
if (!TextUtils.isEmpty(provider.graph.prompt))
authRequestBuilder.setPrompt(provider.graph.prompt);
Intent authIntent = authService.getAuthorizationRequestIntent(authRequestBuilder.build());
Log.i("Graph/contacts intent=" + authIntent);
startActivityForResult(authIntent, ActivitySetup.REQUEST_GRAPH_CONTACTS_OAUTH);
} catch (Throwable ex) {
Log.unexpectedError(getParentFragmentManager(), ex);
}
}
private void onHandleGraphContactsOAuth(@NonNull Intent data) {
try {
Log.i("Graph/contacts authorized");
AuthorizationResponse auth = AuthorizationResponse.fromIntent(data);
if (auth == null) {
AuthorizationException ex = AuthorizationException.fromIntent(data);
if (ex == null)
throw new IllegalArgumentException("No response data");
else
throw ex;
}
final Context context = getContext();
final EmailProvider provider = EmailProvider.getProvider(context, "outlookgraph");
AuthorizationService authService = new AuthorizationService(context);
ClientAuthentication clientAuth;
if (provider.graph.clientSecret == null)
clientAuth = NoClientAuthentication.INSTANCE;
else
clientAuth = new ClientSecretPost(provider.graph.clientSecret);
TokenRequest.Builder builder = new TokenRequest.Builder(
auth.request.configuration,
auth.request.clientId)
.setGrantType(GrantTypeValues.AUTHORIZATION_CODE)
.setRedirectUri(auth.request.redirectUri)
.setCodeVerifier(auth.request.codeVerifier)
.setAuthorizationCode(auth.authorizationCode)
.setNonce(auth.request.nonce);
if (provider.graph.tokenScopes)
builder.setScope(GRAPH_SCOPE_READ_CONTACTS);
authService.performTokenRequest(
builder.build(),
clientAuth,
new AuthorizationService.TokenResponseCallback() {
@Override
public void onTokenRequestCompleted(TokenResponse access, AuthorizationException error) {
try {
if (error != null)
throw error;
if (access == null || access.accessToken == null)
throw new IllegalStateException("No access token");
Log.i("Graph/contacts got token");
int semi = auth.request.state.lastIndexOf(':');
long account = Long.parseLong(auth.request.state.substring(semi + 1));
Bundle args = new Bundle();
args.putLong("account", account);
args.putString("accessToken", access.accessToken);
new SimpleTask<Void>() {
@Override
protected Void onExecute(Context context, Bundle args) throws Throwable {
long account = args.getLong("account");
String accessToken = args.getString("accessToken");
MicrosoftGraph.downloadContacts(context, account, accessToken);
return null;
}
@Override
protected void onExecuted(Bundle args, Void data) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putBoolean("suggest_sent", true).apply();
ToastEx.makeText(context, R.string.title_completed, Toast.LENGTH_LONG).show();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Log.unexpectedError(getParentFragmentManager(), ex);
}
}.execute(FragmentSetup.this, args, "graph:contacts");
} catch (Throwable ex) {
Log.unexpectedError(getParentFragmentManager(), ex);
}
}
});
} catch (Throwable ex) {
Log.unexpectedError(getParentFragmentManager(), ex);
}
}
private ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {

View File

@ -21,6 +21,7 @@ package eu.faircode.email;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Base64OutputStream;
@ -28,7 +29,9 @@ import androidx.preference.PreferenceManager;
import net.openid.appauth.AuthState;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -120,4 +123,69 @@ public class MicrosoftGraph {
db.identity().setIdentityState(ident.id, null);
}
}
static void downloadContacts(Context context, long account, String accessToken) throws IOException, JSONException {
DB db = DB.getInstance(context);
// https://learn.microsoft.com/en-us/graph/api/user-list-contacts?view=graph-rest-1.0&tabs=http
URL url = new URL(MicrosoftGraph.GRAPH_ENDPOINT + "contacts");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setReadTimeout(GRAPH_TIMEOUT * 1000);
connection.setConnectTimeout(GRAPH_TIMEOUT * 1000);
ConnectionHelper.setUserAgent(context, connection);
connection.setRequestProperty("Authorization", "Bearer " + accessToken);
connection.setRequestProperty("Content-Type", "text/plain");
connection.connect();
try {
int status = connection.getResponseCode();
if (status == HttpURLConnection.HTTP_OK) {
String response = Helper.readStream(connection.getInputStream());
JSONObject jroot = new JSONObject(response);
JSONArray jvalue = jroot.getJSONArray("value");
for (int i = 0; i < jvalue.length(); i++) {
JSONObject jcontact = jvalue.getJSONObject(i);
String displayName = jcontact.optString("displayName");
if (TextUtils.isEmpty(displayName))
displayName = null;
if (jcontact.has("emailAddresses")) {
JSONArray jemailAddresses = jcontact.getJSONArray("emailAddresses");
for (int j = 0; j < jemailAddresses.length(); j++) {
JSONObject jemail = jemailAddresses.getJSONObject(j);
String email = jemail.optString("address");
if (!TextUtils.isEmpty(email)) {
EntityContact contact = db.contact().getContact(account, EntityContact.TYPE_TO, email);
EntityLog.log(context, displayName + " <" + email + ">" +
" account=" + account + " exists=" + (contact != null));
if (contact == null) {
contact = new EntityContact();
contact.account = account;
contact.type = EntityContact.TYPE_TO;
contact.email = email;
contact.name = displayName;
contact.times_contacted = 0;
contact.first_contacted = new Date().getTime();
contact.last_contacted = contact.first_contacted;
db.contact().insertContact(contact);
}
}
}
}
}
} else {
String error = "Error " + status + ": " + connection.getResponseMessage();
try {
InputStream is = connection.getErrorStream();
if (is != null)
error += "\n" + Helper.readStream(is);
} catch (Throwable ex) {
Log.w(ex);
}
throw new IOException(error);
}
} finally {
connection.disconnect();
}
}
}

View File

@ -676,6 +676,18 @@
app:drawableTint="?android:attr/textColorLink"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPermissionsWhy" />
<Button
android:id="@+id/btnGraphContacts"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:drawableEnd="@drawable/twotone_cloud_download_24"
android:drawablePadding="6dp"
android:text="@string/title_setup_import_graph_contact"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvImportContacts" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -261,6 +261,7 @@
<string name="title_setup_permissions_notify_explanation">Notification permissions are required to show notifications for new messages in the status bar</string>
<string name="title_setup_permission_why">Which permissions are needed and why?</string>
<string name="title_setup_permission_import_contacts">How can I import contacts?</string>
<string name="title_setup_import_graph_contact">Outlook contacts</string>
<string name="title_setup_doze">Disable battery optimizations</string>
<string name="title_setup_doze_remark">To send and receive email reliably in the background</string>
<string name="title_setup_doze_explanation">Battery optimizations should be disabled for the app to ensure reliable sending and receiving of messages in the background</string>