mirror of https://github.com/M66B/FairEmail.git
Added downloading Outlook contacts
This commit is contained in:
parent
f07a4682dd
commit
b8085a64f1
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue