Initial
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface AllFieldsAreRequired{
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import com.google.gson.JsonIOException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.RequestBody;
|
||||
import okio.BufferedSink;
|
||||
|
||||
public class JsonObjectRequestBody extends RequestBody{
|
||||
private final Object obj;
|
||||
|
||||
public JsonObjectRequestBody(Object obj){
|
||||
this.obj=obj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaType contentType(){
|
||||
return MediaType.get("application/json;charset=utf-8");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(BufferedSink sink) throws IOException{
|
||||
try{
|
||||
OutputStreamWriter writer=new OutputStreamWriter(sink.outputStream(), StandardCharsets.UTF_8);
|
||||
MastodonAPIController.gson.toJson(obj, writer);
|
||||
writer.flush();
|
||||
}catch(JsonIOException x){
|
||||
throw new IOException(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.FieldNamingPolicy;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonIOException;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
|
||||
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.WorkerThread;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
public class MastodonAPIController{
|
||||
private static final String TAG="MastodonAPIController";
|
||||
public static final Gson gson=new GsonBuilder()
|
||||
.disableHtmlEscaping()
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
|
||||
.registerTypeAdapter(Instant.class, new IsoInstantTypeAdapter())
|
||||
.registerTypeAdapter(LocalDate.class, new IsoLocalDateTypeAdapter())
|
||||
.create();
|
||||
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
||||
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
|
||||
|
||||
private AccountSession session;
|
||||
|
||||
static{
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public MastodonAPIController(@Nullable AccountSession session){
|
||||
this.session=session;
|
||||
}
|
||||
|
||||
public <T> void submitRequest(final MastodonAPIRequest<T> req){
|
||||
thread.postRunnable(()->{
|
||||
try{
|
||||
Request.Builder builder=new Request.Builder()
|
||||
.url(req.getURL().toString())
|
||||
.method(req.getMethod(), req.getRequestBody())
|
||||
.header("User-Agent", "MastodonAndroid/"+BuildConfig.VERSION_NAME);
|
||||
|
||||
String token=null;
|
||||
if(session!=null)
|
||||
token=session.token.accessToken;
|
||||
else if(req.token!=null)
|
||||
token=req.token.accessToken;
|
||||
|
||||
if(token!=null)
|
||||
builder.header("Authorization", "Bearer "+token);
|
||||
|
||||
Request hreq=builder.build();
|
||||
Call call=httpClient.newCall(hreq);
|
||||
synchronized(req){
|
||||
req.okhttpCall=call;
|
||||
}
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
|
||||
|
||||
call.enqueue(new Callback(){
|
||||
@Override
|
||||
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
||||
if(call.isCanceled())
|
||||
return;
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed: "+e);
|
||||
synchronized(req){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
req.onError(e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
||||
if(call.isCanceled())
|
||||
return;
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" received response: "+response);
|
||||
synchronized(req){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
try(ResponseBody body=response.body()){
|
||||
Reader reader=body.charStream();
|
||||
if(response.isSuccessful()){
|
||||
T respObj;
|
||||
try{
|
||||
if(req.respTypeToken!=null)
|
||||
respObj=gson.fromJson(reader, req.respTypeToken.getType());
|
||||
else
|
||||
respObj=gson.fromJson(reader, req.respClass);
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
||||
req.onError(x.getLocalizedMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
try{
|
||||
req.validateAndPostprocessResponse(respObj);
|
||||
}catch(IOException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
||||
req.onError(x.getLocalizedMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" parsed successfully: "+respObj);
|
||||
|
||||
req.onSuccess(respObj);
|
||||
}else{
|
||||
try{
|
||||
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
||||
req.onError(error.get("error").getAsString());
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
req.onError(response.code()+" "+response.message());
|
||||
}catch(IllegalStateException x){
|
||||
req.onError("Error parsing an API error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}catch(Exception x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
|
||||
req.onError(x.getLocalizedMessage());
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
|
||||
private String domain;
|
||||
private AccountSession account;
|
||||
private String path;
|
||||
private String method;
|
||||
private Object requestBody;
|
||||
private Map<String, String> queryParams;
|
||||
Class<T> respClass;
|
||||
TypeToken<T> respTypeToken;
|
||||
Call okhttpCall;
|
||||
Token token;
|
||||
|
||||
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
|
||||
this.path=path;
|
||||
this.method=method.toString();
|
||||
this.respClass=respClass;
|
||||
}
|
||||
|
||||
public MastodonAPIRequest(HttpMethod method, String path, TypeToken<T> respTypeToken){
|
||||
this.path=path;
|
||||
this.method=method.toString();
|
||||
this.respTypeToken=respTypeToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void cancel(){
|
||||
if(okhttpCall!=null){
|
||||
okhttpCall.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public APIRequest<T> exec(){
|
||||
throw new UnsupportedOperationException("Use exec(accountID) or execNoAuth(domain)");
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> exec(String accountID){
|
||||
account=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
domain=account.domain;
|
||||
account.getApiController().submitRequest(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> execNoAuth(String domain){
|
||||
this.domain=domain;
|
||||
AccountSessionManager.getInstance().getUnauthenticatedApiController().submitRequest(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> exec(String domain, Token token){
|
||||
this.domain=domain;
|
||||
this.token=token;
|
||||
AccountSessionManager.getInstance().getUnauthenticatedApiController().submitRequest(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
protected void setRequestBody(Object body){
|
||||
requestBody=body;
|
||||
}
|
||||
|
||||
protected void addQueryParameter(String key, String value){
|
||||
if(queryParams==null)
|
||||
queryParams=new HashMap<>();
|
||||
queryParams.put(key, value);
|
||||
}
|
||||
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v1";
|
||||
}
|
||||
|
||||
public Uri getURL(){
|
||||
Uri.Builder builder=new Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority(domain)
|
||||
.path(getPathPrefix()+path);
|
||||
if(queryParams!=null){
|
||||
for(Map.Entry<String, String> param:queryParams.entrySet()){
|
||||
builder.appendQueryParameter(param.getKey(), param.getValue());
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public String getMethod(){
|
||||
return method;
|
||||
}
|
||||
|
||||
public RequestBody getRequestBody(){
|
||||
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MastodonAPIRequest<T> setCallback(Callback<T> callback){
|
||||
super.setCallback(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
public void validateAndPostprocessResponse(T respObj) throws IOException{
|
||||
if(respObj instanceof BaseModel){
|
||||
((BaseModel) respObj).postprocess();
|
||||
}else if(respObj instanceof List){
|
||||
for(Object item : ((List) respObj)){
|
||||
if(item instanceof BaseModel)
|
||||
((BaseModel) item).postprocess();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onError(String msg){
|
||||
invokeErrorCallback(new MastodonErrorResponse(msg));
|
||||
}
|
||||
|
||||
void onSuccess(T resp){
|
||||
invokeSuccessCallback(resp);
|
||||
}
|
||||
|
||||
public enum HttpMethod{
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class MastodonErrorResponse extends ErrorResponse{
|
||||
public final String error;
|
||||
|
||||
public MastodonErrorResponse(String error){
|
||||
this.error=error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindErrorView(View view){
|
||||
TextView text=view.findViewById(R.id.error_text);
|
||||
text.setText(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showToast(Context context){
|
||||
Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ObjectValidationException extends IOException{
|
||||
public ObjectValidationException(){
|
||||
}
|
||||
|
||||
public ObjectValidationException(String message){
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ObjectValidationException(String message, Throwable cause){
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ObjectValidationException(Throwable cause){
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface RequiredField{
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.joinmastodon.android.api.gson;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
public class IsoInstantTypeAdapter extends TypeAdapter<Instant>{
|
||||
@Override
|
||||
public void write(JsonWriter out, Instant value) throws IOException{
|
||||
if(value==null)
|
||||
out.nullValue();
|
||||
else
|
||||
out.value(DateTimeFormatter.ISO_INSTANT.format(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant read(JsonReader in) throws IOException{
|
||||
if(in.peek()==JsonToken.NULL){
|
||||
in.nextNull();
|
||||
return null;
|
||||
}
|
||||
try{
|
||||
return DateTimeFormatter.ISO_INSTANT.parse(in.nextString(), Instant::from);
|
||||
}catch(DateTimeParseException x){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.joinmastodon.android.api.gson;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
public class IsoLocalDateTypeAdapter extends TypeAdapter<LocalDate>{
|
||||
@Override
|
||||
public void write(JsonWriter out, LocalDate value) throws IOException{
|
||||
if(value==null)
|
||||
out.nullValue();
|
||||
else
|
||||
out.value(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate read(JsonReader in) throws IOException{
|
||||
if(in.peek()==JsonToken.NULL){
|
||||
in.nextNull();
|
||||
return null;
|
||||
}
|
||||
try{
|
||||
return LocalDate.parse(in.nextString());
|
||||
}catch(DateTimeParseException x){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
|
||||
public class GetInstance extends MastodonAPIRequest<Instance>{
|
||||
public GetInstance(){
|
||||
super(HttpMethod.GET, "/instance", Instance.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class GetOwnAccount extends MastodonAPIRequest<Account>{
|
||||
public GetOwnAccount(){
|
||||
super(HttpMethod.GET, "/accounts/verify_credentials", Account.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.joinmastodon.android.api.requests.catalog;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.catalog.CatalogCategory;
|
||||
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetCatalogCategories extends MastodonAPIRequest<List<CatalogCategory>>{
|
||||
private String lang;
|
||||
|
||||
public GetCatalogCategories(String lang){
|
||||
super(HttpMethod.GET, null, new TypeToken<>(){});
|
||||
this.lang=lang;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getURL(){
|
||||
Uri.Builder builder=new Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority("api.joinmastodon.org")
|
||||
.path("/categories");
|
||||
if(!TextUtils.isEmpty(lang))
|
||||
builder.appendQueryParameter("language", lang);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.joinmastodon.android.api.requests.catalog;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetCatalogInstances extends MastodonAPIRequest<List<CatalogInstance>>{
|
||||
|
||||
private String lang, category;
|
||||
|
||||
public GetCatalogInstances(String lang, String category){
|
||||
super(HttpMethod.GET, null, new TypeToken<>(){});
|
||||
this.lang=lang;
|
||||
this.category=category;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getURL(){
|
||||
Uri.Builder builder=new Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority("api.joinmastodon.org")
|
||||
.path("/servers");
|
||||
if(!TextUtils.isEmpty(lang))
|
||||
builder.appendQueryParameter("language", lang);
|
||||
if(!TextUtils.isEmpty(category))
|
||||
builder.appendQueryParameter("category", category);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.joinmastodon.android.api.requests.oauth;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
|
||||
public class CreateOAuthApp extends MastodonAPIRequest<Application>{
|
||||
public CreateOAuthApp(){
|
||||
super(HttpMethod.POST, "/apps", Application.class);
|
||||
setRequestBody(new Request());
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public String clientName="Mastodon for Android";
|
||||
public String redirectUris=AccountSessionManager.REDIRECT_URI;
|
||||
public String scopes=AccountSessionManager.SCOPE;
|
||||
public String website="https://joinmastodon.org";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.joinmastodon.android.api.requests.oauth;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
public class GetOauthToken extends MastodonAPIRequest<Token>{
|
||||
public GetOauthToken(String clientID, String clientSecret, String code){
|
||||
super(HttpMethod.POST, "/oauth/token", Token.class);
|
||||
setRequestBody(new Request(clientID, clientSecret, code));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "";
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public String grantType="authorization_code";
|
||||
public String clientId;
|
||||
public String clientSecret;
|
||||
public String redirectUri=AccountSessionManager.REDIRECT_URI;
|
||||
public String scope=AccountSessionManager.SCOPE;
|
||||
public String code;
|
||||
|
||||
public Request(String clientId, String clientSecret, String code){
|
||||
this.clientId=clientId;
|
||||
this.clientSecret=clientSecret;
|
||||
this.code=code;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
public class AccountSession{
|
||||
public Token token;
|
||||
public Account self;
|
||||
public String domain;
|
||||
public int tootCharLimit;
|
||||
public Application app;
|
||||
private transient MastodonAPIController apiController;
|
||||
|
||||
AccountSession(Token token, Account self, Application app, String domain, int tootCharLimit){
|
||||
this.token=token;
|
||||
this.self=self;
|
||||
this.domain=domain;
|
||||
this.app=app;
|
||||
this.tootCharLimit=tootCharLimit;
|
||||
}
|
||||
|
||||
AccountSession(){}
|
||||
|
||||
public String getID(){
|
||||
return domain+"_"+self.id;
|
||||
}
|
||||
|
||||
public MastodonAPIController getApiController(){
|
||||
if(apiController==null)
|
||||
apiController=new MastodonAPIController(this);
|
||||
return apiController;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.OAuthActivity;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class AccountSessionManager{
|
||||
private static final String TAG="AccountSessionManager";
|
||||
public static final String SCOPE="read write follow push";
|
||||
public static final String REDIRECT_URI="mastodon-android-auth://callback";
|
||||
|
||||
private static final AccountSessionManager instance=new AccountSessionManager();
|
||||
|
||||
private HashMap<String, AccountSession> sessions=new HashMap<>();
|
||||
private MastodonAPIController unauthenticatedApiController=new MastodonAPIController(null);
|
||||
private Instance authenticatingInstance;
|
||||
private Application authenticatingApp;
|
||||
private String lastActiveAccountID;
|
||||
private SharedPreferences prefs;
|
||||
|
||||
public static AccountSessionManager getInstance(){
|
||||
return instance;
|
||||
}
|
||||
|
||||
private AccountSessionManager(){
|
||||
prefs=MastodonApp.context.getSharedPreferences("account_manager", Context.MODE_PRIVATE);
|
||||
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||
if(!file.exists())
|
||||
return;
|
||||
try(FileInputStream in=new FileInputStream(file)){
|
||||
SessionsStorageWrapper w=MastodonAPIController.gson.fromJson(new InputStreamReader(in, StandardCharsets.UTF_8), SessionsStorageWrapper.class);
|
||||
for(AccountSession session:w.accounts){
|
||||
sessions.put(session.getID(), session);
|
||||
}
|
||||
}catch(IOException x){
|
||||
Log.e(TAG, "Error loading accounts", x);
|
||||
}
|
||||
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
|
||||
}
|
||||
|
||||
public void addAccount(Instance instance, Token token, Account self, Application app){
|
||||
AccountSession session=new AccountSession(token, self, app, instance.uri, instance.maxTootChars);
|
||||
sessions.put(session.getID(), session);
|
||||
lastActiveAccountID=session.getID();
|
||||
writeAccountsFile();
|
||||
}
|
||||
|
||||
private void writeAccountsFile(){
|
||||
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||
try{
|
||||
try(FileOutputStream out=new FileOutputStream(file)){
|
||||
SessionsStorageWrapper w=new SessionsStorageWrapper();
|
||||
w.accounts=new ArrayList<>(sessions.values());
|
||||
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||
MastodonAPIController.gson.toJson(w, writer);
|
||||
writer.flush();
|
||||
}
|
||||
}catch(IOException x){
|
||||
Log.e(TAG, "Error writing accounts file", x);
|
||||
}
|
||||
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<AccountSession> getLoggedInAccounts(){
|
||||
return new ArrayList<>(sessions.values());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AccountSession getAccount(String id){
|
||||
AccountSession session=sessions.get(id);
|
||||
if(session==null)
|
||||
throw new IllegalStateException("Account session "+id+" not found");
|
||||
return session;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AccountSession getLastActiveAccount(){
|
||||
if(sessions.isEmpty() || lastActiveAccountID==null)
|
||||
return null;
|
||||
return getAccount(lastActiveAccountID);
|
||||
}
|
||||
|
||||
public String getLastActiveAccountID(){
|
||||
return lastActiveAccountID;
|
||||
}
|
||||
|
||||
public void removeAccount(String id){
|
||||
AccountSession session=getAccount(id);
|
||||
sessions.remove(id);
|
||||
if(lastActiveAccountID.equals(id)){
|
||||
if(sessions.isEmpty())
|
||||
lastActiveAccountID=null;
|
||||
else
|
||||
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
|
||||
}
|
||||
writeAccountsFile();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MastodonAPIController getUnauthenticatedApiController(){
|
||||
return unauthenticatedApiController;
|
||||
}
|
||||
|
||||
public void authenticate(Context context, Instance instance){
|
||||
authenticatingInstance=instance;
|
||||
ProgressDialog progress=new ProgressDialog(context);
|
||||
progress.setMessage(context.getString(R.string.preparing_auth));
|
||||
progress.setCancelable(false);
|
||||
progress.show();
|
||||
new CreateOAuthApp()
|
||||
.setCallback(new Callback<Application>(){
|
||||
@Override
|
||||
public void onSuccess(Application result){
|
||||
authenticatingApp=result;
|
||||
progress.dismiss();
|
||||
Uri uri=new Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority(instance.uri)
|
||||
.path("/oauth/authorize")
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("client_id", result.clientId)
|
||||
.appendQueryParameter("redirect_uri", "mastodon-android-auth://callback")
|
||||
.appendQueryParameter("scope", "read write follow push")
|
||||
.build();
|
||||
|
||||
new CustomTabsIntent.Builder()
|
||||
.build()
|
||||
.launchUrl(context, uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(context);
|
||||
progress.dismiss();
|
||||
}
|
||||
})
|
||||
.execNoAuth(instance.uri);
|
||||
}
|
||||
|
||||
public Instance getAuthenticatingInstance(){
|
||||
return authenticatingInstance;
|
||||
}
|
||||
|
||||
public Application getAuthenticatingApp(){
|
||||
return authenticatingApp;
|
||||
}
|
||||
|
||||
private static class SessionsStorageWrapper{
|
||||
public List<AccountSession> accounts;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user