web-dev-qa-db-fra.com

Retrofit: Rediriger vers LoginActivity si le code de réponse est 401

Comment démarrer LoginActivity à partir de l'intercepteur (classe de non-activité)? J'ai essayé le code (Interceptor) ci-dessous mais je ne travaille pas pour moi. 

Intercepteur

OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {

                Request newRequest = chain.request().newBuilder()
                        .addHeader("Authorization", "Bearer " + auth_token_string)
                        .build();

                Response response =  chain.proceed(newRequest);
                Log.d("MyApp", "Code : "+response.code());
                if (response.code() == 401){
                    Intent intent = new Intent(SplashActivity.getContextOfApplication(), LoginActivity.class);
                    startActivity(intent);
                    finish();  //Not working
                    return response;
                }

                return chain.proceed(newRequest);
            }
        }).build();

C’est la solution actuelle que j’utilise. Existe-t-il une meilleure solution? Cette solution doit être répétée à chaque appel de l'API.

Activité principale

call.enqueue(new Callback<Token>() {
            @Override
            public void onResponse(Call<Token> call, Response<Token> response) {
                if(response.isSuccessful())
                {
                    //success
                }
                else
                {
                    Intent intent = new Intent(MainActivity.this.getApplicationContext(), LoginActivity.class);
                    startActivity(intent);
                    finish();
                }
            }
            @Override
            public void onFailure(Call<Token> call, Throwable t) {

            }
        });
15
Rick

Pensez à introduire une implémentation personnalisée de l'interface retrofit2.Callback, par exemple. BaseCallback:

public abstract class BaseCallback<T> implements Callback<T> {

    private final Context context;

    public BaseCallback(Context context) {
        this.context = context;
    }

    @Override
    public void onResponse(Call<T> call, Response<T> response) {
        if (response.code() == 401) {
            // launch login activity using `this.context`
        } else {
            onSuccess(response.body());
        }
    }

    @Override
    public void onFailure(Call<T> call, Throwable t) {

    }

    abstract void onSuccess(T response);

}

Maintenant, depuis le site de l'appelant, vous devriez changer new Callback<Token> avec new BaseCallback<Token>:

call.enqueue(new BaseCallback<Token>(context) {
    @Override
    void onSuccess(Token response) {
        // do something with token
    }
});

Bien que cette approche ne réponde pas à votre affirmation suivante:

donc je n'ai pas à continuer à répéter le même code pour chaque appel d'api

néanmoins, je ne peux pas proposer une meilleure approche.

7
azizbekian

Personnellement, je voudrais suggérer l’utilisation du modèle de bus d’événements ici. Vous pouvez utiliser implémentation de greenrobot ou ce que vous voulez, puisqu'il s'agit davantage d'une approche d'architecture que d'une implémentation concrète. 

  1. Créer un modèle d'événement

    public class UnauthorizedEvent {
    
        private static final UnauthorizedEvent INSTANCE = new UnauthorizedEvent();
    
        public static UnauthorizedEvent instance() {
            return INSTANCE;
        }
    
        private UnauthorizedEvent() {
        }
    }
    
  2. Implémentation de la variable personnalisée Interceptor qui décompresse l'événement concernant les requêtes non autorisées

    class UnauthorizedInterceptor implements Interceptor {
    
        @Override
        public Response intercept(@NonNull Chain chain) throws IOException {
            Response response = chain.proceed(chain.request());
            if (response.code() == 401) {
                EventBus.getDefault().post(UnauthorizedEvent.instance());
            }
            return response;
        }
    }
    
  3. Créez la classe BaseActivity qui gère UnauthorizedEvent

    public class BaseActivity extends Activity {
    
        @Override
        public void onStart() {
            super.onStart();
            EventBus.getDefault().register(this);
        }
    
        @Override
        public void onStop() {
            super.onStop();
            EventBus.getDefault().unregister(this);
        }
    
        @Subscribe
        public final void onUnauthorizedEvent(UnauthorizedEvent e) {
            handleUnauthorizedEvent();
        }
    
        protected void handleUnauthorizedEvent() {
            Intent intent = new Intent(this, LoginActivity.class);
            startActivity(intent);
        }
    }
    
  4. Empêcher le lancement de LoginActivity à partir de LoginActivity

    public class LoginActivty extends BaseActivity {
    
        @Override
        protected void handleUnauthorizedEvent() {
            //Don't handle unauthorized event
        }
    }
    

    Une autre approche consiste à ne pas prolonger BaseActivity ici.

  5. Enregistrez votre intercepteur

    OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(new UnauthorizedInterceptor())
            .build();
    

Avantages:

  • Couplage lâche entre les composants
  • Étendre facilement la logique en remplaçant handleUnauthorizedEvent
  • Pas besoin de réécrire le code pour utiliser de nouveaux types de callbacks
  • Réduire le facteur humain pour faire des erreurs (en utilisant Callback au lieu de BaseCallback)

Les inconvénients:

  • Le modèle EventBus rend le débogage plus compliqué
  • Une dépendance supplémentaire ou une propre implémentation qui apporte un nouveau code au projet

Notez également que cet exemple ne couvre pas les problèmes de multithreading. Cela résout votre problème de traitement des demandes non autorisées. Ainsi, si deux requêtes reçoivent 401, il est possible que 2 instances de LoginActivity soient démarrées.

2
hluhovskyi

Solution généralisée: Vous pouvez la résoudre en généralisant le traitement des erreurs. Vous pouvez utiliser CallAdapterFactory personnalisé pour le générateur Retrofit. Veuillez vous référer aux cours ci-dessous:

RxErrorHandlingCallAdapterFactory:

public class RxErrorHandlingCallAdapterFactory extends CallAdapter.Factory {
    private static Context mContext = null;
    private final RxJava2CallAdapterFactory original;

    private RxErrorHandlingCallAdapterFactory() {
        original = RxJava2CallAdapterFactory.create();
    }

    public static CallAdapter.Factory create(Context context) {
        mContext = context;
        return new RxErrorHandlingCallAdapterFactory();
    }

    @Override
    public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
        return new RxCallAdapterWrapper(retrofit, original.get(returnType, annotations, retrofit));
    }

    private static class RxCallAdapterWrapper<R> implements CallAdapter<R, Object> {
        private final Retrofit retrofit;
        private final CallAdapter<R,
                Object> wrapped;

        public RxCallAdapterWrapper(Retrofit retrofit, CallAdapter<R, Object> wrapped) {
            this.retrofit = retrofit;
            this.wrapped = wrapped;
        }

        @Override
        public Type responseType() {
            return wrapped.responseType();
        }

        @Override
        public Object adapt(Call<R> call) {
            Object result = wrapped.adapt(call);
            if (result instanceof Single) {
                return ((Single) result).onErrorResumeNext(new Function<Throwable, SingleSource>() {
                    @Override
                    public SingleSource apply(@NonNull Throwable throwable) throws Exception {
                        return Single.error(asRetrofitException(throwable));
                    }
                });
            }
            if (result instanceof Observable) {
                return ((Observable) result).onErrorResumeNext(new Function<Throwable, ObservableSource>() {
                    @Override
                    public ObservableSource apply(@NonNull Throwable throwable) throws Exception {
                        return Observable.error(asRetrofitException(throwable));
                    }
                });
            }
            if (result instanceof Completable) {
                return ((Completable) result).onErrorResumeNext(new Function<Throwable, CompletableSource>() {
                    @Override
                    public CompletableSource apply(@NonNull Throwable throwable) throws Exception {
                        return Completable.error(asRetrofitException(throwable));
                    }
                });
            }
            return result;
        }

        private RetrofitException asRetrofitException(Throwable throwable) {
            // We had non-200 http error
            Log.v("log", "eror");
            throwable.printStackTrace();
            if (throwable instanceof HttpException) {
                HttpException httpException = (HttpException) throwable;
                final Response response = httpException.response();


                //if ((mContext instanceof Activity)) {

                String s = "Something went wrong."; //mContext.getString(R.string.something_went_wrong);
                try {
                    s = new JSONObject(response.errorBody().string()).getString("message");
                    if (response.code() == 401) { // 401 Unauthorized
                        Intent intent = new Intent(mContext, LoginActivity.class);
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
                        mContext.startActivity(intent);
                    }
                } catch (JSONException | IOException e) {
                    e.printStackTrace();
                }

                return RetrofitException.unexpectedError(s, response, retrofit);

                //showErrorDialog(mContext, response);
                //}

//                return RetrofitException.httpError(response.errorBody().toString(), response, retrofit);
            }
            // A network error happened
            if (throwable instanceof IOException) {
                return RetrofitException.networkError((IOException) throwable);
            }
            // We don't know what happened. We need to simply convert to an unknown error
            return RetrofitException.unexpectedError(throwable);
        }
    }
}

RetrofitException:

public class RetrofitException extends RuntimeException {
    private final String url;
    private final Response response;
    private final Kind kind;
    private final Retrofit retrofit;

    RetrofitException(String message, String url, Response response, Kind kind, Throwable exception, Retrofit retrofit) {
        super(message, exception);
        this.url = url;
        this.response = response;
        this.kind = kind;
        this.retrofit = retrofit;
    }

    public static RetrofitException httpError(String url, Response response, Retrofit retrofit) {
        String message = response.code() + " " + response.message();
        return new RetrofitException(message, url, response, Kind.HTTP, null, retrofit);
    }

    public static RetrofitException networkError(IOException exception) {
        return new RetrofitException(exception.getMessage(), null, null, Kind.NETWORK, exception, null);
    }

    public static RetrofitException unexpectedError(Throwable exception) {
        return new RetrofitException(exception.getMessage(), null, null, Kind.UNEXPECTED, exception, null);
    }

    public static RetrofitException unexpectedError(String s, Response response, Retrofit retrofit) {
        return new RetrofitException(s, null, null, Kind.UNEXPECTED, null, null);
    }

    /**
     * The request URL which produced the error.
     */
    public String getUrl() {
        return url;
    }

    /**
     * Response object containing status code, headers, body, etc.
     */
    public Response getResponse() {
        return response;
    }

    /**
     * The event kind which triggered this error.
     */
    public Kind getKind() {
        return kind;
    }

    /**
     * The Retrofit this request was executed on
     */
    public Retrofit getRetrofit() {
        return retrofit;
    }

    /**
     * HTTP response body converted to specified {@code type}. {@code null} if there is no
     * response.
     *
     * @throws IOException if unable to convert the body to the specified {@code type}.
     */
    public <T> T getErrorBodyAs(Class<T> type) throws IOException {
        if (response == null || response.errorBody() == null) {
            return null;
        }
        Converter<ResponseBody, T> converter = retrofit.responseBodyConverter(type, new Annotation[0]);
        return converter.convert(response.errorBody());
    }

    /**
     * Identifies the event kind which triggered a {@link RetrofitException}.
     */
    public enum Kind {
        /**
         * An {@link IOException} occurred while communicating to the server.
         */
        NETWORK,
        /**
         * A non-200 HTTP status code was received from the server.
         */
        HTTP,
        /**
         * An internal error occurred while attempting to execute a request. It is best practice to
         * re-throw this exception so your application crashes.
         */
        UNEXPECTED
    }
}

Retrofit Builder:

Retrofit retrofit = new Retrofit.Builder()
            .addCallAdapterFactory(RxErrorHandlingCallAdapterFactory.create(context))
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(API_URL)
                .client(client)
                .build();

Vous pouvez gérer 401 dans RxErrorHandlingCallAdapterFactory et d'autres erreurs via Throwable.

1
SANAT

Le moyen le plus simple consiste à injecter un contexte d'activité dans l'instance d'Interceptor . Si vous utilisez des outils d'identification directe, tels que Dagger2 ou Toothpick, ce sera très simple. Je recommande d'utiliser un cure-dent) 

https://github.com/stephanenicolas/toothpick

Le code le plus proche sera dans kotlin, car c’est mon code standard. Ceux qui pensent que vous avez besoin de résoudre votre problème, j’écrirai en Java.

La solution sera comme ça: 

@Qualifier
annotation class BackendUrl


class ActivityModule(activity: BaseActivity): Module() {

    init {
        bind(Activity::class.Java).toInstance(activity)
    }

}

class NetworkModule: Module() {

    init {
        bind(String::class.Java).withName(BackendUrl::class.Java).toInstance(Constants.URL)
        bind(Gson::class.Java).toInstance(GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create())
        bind(CacheHolder::class.Java).toProvider(CacheProvider::class.Java).singletonInScope()
        bind(OkHttpClient::class.Java).toProvider(OkHttpProvider::class.Java).instancesInScope()
        bind(BackendApi::class.Java).toProvider(BackendApiProvider::class.Java).instancesInScope()
        bind(RedirectInterceptor::class.Java).to(RedirectInterceptor::class.Java)
    }

}

Que vous devez créer Providers pour la dépendance à l'injection

class BackendApiProvider @Inject constructor(
        private val okHttpClient: OkHttpClient,
        private val gson: Gson,
        @BackendUrl private val serverPath: String
) : Provider<BackendApi> {

    override fun get() =
            Retrofit.Builder()
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .client(okHttpClient)
                    .baseUrl(serverPath)
                    .build()
                    .create(BackendApi::class.Java)
}

Et votre intercepteur de redirection:

public class RedirectInterceptor implements Interceptor {

    private final Context context;

    @Inject
    public RedirectInterceptor(Activity context) {
        this.context = context;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request newRequest = chain.request().newBuilder()
                .build();

        Response response =  chain.proceed(newRequest);
        Log.d("MyApp", "Code : "+response.code());
        if (response.code() == 401){
            Intent intent = new Intent(context, LoginActivity.class);
            context.startActivity(intent);
            ((Activity) context).finish();
            return response;
        }

        return chain.proceed(newRequest);
    }
}

Oh oui. Pour l'autorisation, il sera préférable de créer une nouvelle instance d'un autre intercepteur.

class HeaderInterceptor(private val token: String?) : Interceptor {

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val newRequest =  request.newBuilder()

        Log.d(TAG, "token: $token")

        if (token != null && token.isNotBlank()) {
            newRequest.addHeader("Authorization", "Bearer $token")
        }

        return chain.proceed(newRequest.build())
    }

    companion object {
        private val TAG = HeaderInterceptor::class.Java.toString()
    }

} 

Et votre OkhttpProvder

class OkHttpProvider @Inject constructor(cacheHolder: CacheHolder, prefs: IPreferences, redirectInterceptor: RedirectInterceptor) : Provider<OkHttpClient> {

    private val client: OkHttpClient

    init {

        val builder = OkHttpClient.Builder()
        builder
                .addNetworkInterceptor(redirectInterceptor)
                .addNetworkInterceptor(HeaderInterceptor(prefs.getAuthToken()))
                .readTimeout(30, TimeUnit.SECONDS)
                .cache(cacheHolder.okHttpCache)

        client = builder.build()
    }

    override fun get() = client
}

C'est tout! Maintenant, il vous suffit de placer vos modules dans les bonnes portées. 

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.init_view)      

        Toothpick.openScopes("activity scope").apply {
            installModules(ActivityModule(this@YourActivity))
            Toothpick.inject(this@YourActivity, this)
        }

        Toothpick.openScopes("activity scope", "network scope").apply {
            installModules(NetworkModule())
        }

        // your activity code
    }
1
Scrobot