Dependency injection improvement (#596)

* inject MastodonApi into LoginActivity

* inject AccountManager into MainActivity

* inject AccountManager into SplashActivity, convert to Kotlin

* inject AccountManager into AccountActivity

* inject AccountManager into LoginActivity

* inject AccountManager into NotificationsFragment and NotificationClearBroadcastReceiver, fix MainActivity

* ooops

* use same OkHttpClient for Retrofit & Picasso

* fix ordering of okhttp interceptors

* remove dependencies on TuskyApplication

* bugfix
This commit is contained in:
Konrad Pozniak 2018-04-22 17:20:01 +02:00 committed by GitHub
parent d17ff3eb0f
commit b4ba457d89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 196 additions and 176 deletions

View File

@ -88,6 +88,8 @@ public final class AccountActivity extends BaseActivity implements ActionButtonA
@Inject @Inject
public MastodonApi mastodonApi; public MastodonApi mastodonApi;
@Inject @Inject
public AccountManager accountManager;
@Inject
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector; public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
private String accountId; private String accountId;
@ -207,8 +209,7 @@ public final class AccountActivity extends BaseActivity implements ActionButtonA
// Obtain information to fill out the profile. // Obtain information to fill out the profile.
obtainAccount(); obtainAccount();
AccountEntity activeAccount = TuskyApplication.getInstance(this).getServiceLocator() AccountEntity activeAccount = accountManager.getActiveAccount();
.get(AccountManager.class).getActiveAccount();
if (accountId.equals(activeAccount.getAccountId())) { if (accountId.equals(activeAccount.getAccountId())) {
isSelf = true; isSelf = true;

View File

@ -45,8 +45,8 @@ public abstract class BaseActivity extends AppCompatActivity {
/* There isn't presently a way to globally change the theme of a whole application at /* There isn't presently a way to globally change the theme of a whole application at
* runtime, just individual activities. So, each activity has to set its theme before any * runtime, just individual activities. So, each activity has to set its theme before any
* views are created. */ * views are created. */
String theme = preferences.getString("appTheme", TuskyApplication.APP_THEME_DEFAULT); String theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT);
ThemeUtils.setAppNightMode(theme); ThemeUtils.setAppNightMode(theme, this);
int style; int style;
switch (preferences.getString("statusTextSize", "medium")) { switch (preferences.getString("statusTextSize", "medium")) {

View File

@ -32,21 +32,26 @@ import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.AccessToken import com.keylesspalace.tusky.entity.AccessToken
import com.keylesspalace.tusky.entity.AppCredentials import com.keylesspalace.tusky.entity.AppCredentials
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.CustomTabsHelper import com.keylesspalace.tusky.util.CustomTabsHelper
import com.keylesspalace.tusky.util.OkHttpUtils
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import kotlinx.android.synthetic.main.activity_login.* import kotlinx.android.synthetic.main.activity_login.*
import okhttp3.HttpUrl
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import retrofit2.Retrofit import javax.inject.Inject
import retrofit2.converter.gson.GsonConverterFactory
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity(), Injectable {
@Inject
lateinit var mastodonApi: MastodonApi
@Inject
lateinit var accountManager: AccountManager
private lateinit var preferences: SharedPreferences private lateinit var preferences: SharedPreferences
private var domain: String = "" private var domain: String = ""
@ -64,8 +69,8 @@ class LoginActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
preferences = PreferenceManager.getDefaultSharedPreferences(this) preferences = PreferenceManager.getDefaultSharedPreferences(this)
val theme = preferences.getString("appTheme", TuskyApplication.APP_THEME_DEFAULT) val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
ThemeUtils.setAppNightMode(theme) ThemeUtils.setAppNightMode(theme, this)
setContentView(R.layout.activity_login) setContentView(R.layout.activity_login)
@ -114,16 +119,6 @@ class LoginActivity : AppCompatActivity() {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
private fun getApiFor(domain: String): MastodonApi {
val retrofit = Retrofit.Builder()
.baseUrl("https://" + domain)
.client(OkHttpUtils.getCompatibleClient(preferences))
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(MastodonApi::class.java)
}
/** /**
* Obtain the oauth client credentials for this app. This is only necessary the first time the * Obtain the oauth client credentials for this app. This is only necessary the first time the
* app is run on a given server instance. So, after the first authentication, they are * app is run on a given server instance. So, after the first authentication, they are
@ -133,7 +128,15 @@ class LoginActivity : AppCompatActivity() {
loginButton.isEnabled = false loginButton.isEnabled = false
domain = validateDomain(domainEditText.text.toString()) domain = canonicalizeDomain(domainEditText.text.toString())
try {
HttpUrl.Builder().host(domain).scheme("https").build()
} catch (e: IllegalArgumentException) {
setLoading(false)
domainEditText.error = getString(R.string.error_invalid_domain)
return
}
val callback = object : Callback<AppCredentials> { val callback = object : Callback<AppCredentials> {
override fun onResponse(call: Call<AppCredentials>, override fun onResponse(call: Call<AppCredentials>,
@ -159,16 +162,11 @@ class LoginActivity : AppCompatActivity() {
} }
} }
try { mastodonApi
getApiFor(domain) .authenticateApp(domain, getString(R.string.app_name), oauthRedirectUri,
.authenticateApp(getString(R.string.app_name), oauthRedirectUri, OAUTH_SCOPES, getString(R.string.app_website))
OAUTH_SCOPES, getString(R.string.app_website)) .enqueue(callback)
.enqueue(callback) setLoading(true)
setLoading(true)
} catch (e: IllegalArgumentException) {
setLoading(false)
domainEditText.error = getString(R.string.error_invalid_domain)
}
} }
@ -250,7 +248,7 @@ class LoginActivity : AppCompatActivity() {
} }
} }
getApiFor(domain).fetchOAuthToken(clientId, clientSecret, redirectUri, code, mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code,
"authorization_code").enqueue(callback) "authorization_code").enqueue(callback)
} else if (error != null) { } else if (error != null) {
/* Authorization failed. Put the error response where the user can read it and they /* Authorization failed. Put the error response where the user can read it and they
@ -290,9 +288,7 @@ class LoginActivity : AppCompatActivity() {
setLoading(true) setLoading(true)
TuskyApplication.getInstance(this).serviceLocator accountManager.addAccount(accessToken, domain)
.get(AccountManager::class.java)
.addAccount(accessToken, domain)
val intent = Intent(this, MainActivity::class.java) val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
@ -316,14 +312,10 @@ class LoginActivity : AppCompatActivity() {
} }
/** Make sure the user-entered text is just a fully-qualified domain name. */ /** Make sure the user-entered text is just a fully-qualified domain name. */
private fun validateDomain(domain: String): String { private fun canonicalizeDomain(domain: String): String {
// Strip any schemes out. // Strip any schemes out.
var s = domain.replaceFirst("http://", "") var s = domain.replaceFirst("http://", "")
s = s.replaceFirst("https://", "") s = s.replaceFirst("https://", "")
//strip out any slashes that might have been added
s = s.replace("/", "")
// If a username was included (e.g. username@example.com), just take what's after the '@'. // If a username was included (e.g. username@example.com), just take what's after the '@'.
val at = s.lastIndexOf('@') val at = s.lastIndexOf('@')
if (at != -1) { if (at != -1) {

View File

@ -92,11 +92,11 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity,
public MastodonApi mastodonApi; public MastodonApi mastodonApi;
@Inject @Inject
public DispatchingAndroidInjector<Fragment> fragmentInjector; public DispatchingAndroidInjector<Fragment> fragmentInjector;
@Inject
public AccountManager accountManager;
private static int COMPOSE_RESULT = 1; private static int COMPOSE_RESULT = 1;
AccountManager accountManager;
private FloatingActionButton composeButton; private FloatingActionButton composeButton;
private AccountHeader headerResult; private AccountHeader headerResult;
private Drawer drawer; private Drawer drawer;
@ -107,9 +107,6 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity,
Intent intent = getIntent(); Intent intent = getIntent();
int tabPosition = 0; int tabPosition = 0;
accountManager = TuskyApplication.getInstance(this).getServiceLocator()
.get(AccountManager.class);
if (intent != null) { if (intent != null) {
long accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1); long accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1);

View File

@ -104,8 +104,8 @@ public class PreferencesActivity extends BaseActivity
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
switch (key) { switch (key) {
case "appTheme": { case "appTheme": {
String theme = sharedPreferences.getString("appTheme", TuskyApplication.APP_THEME_DEFAULT); String theme = sharedPreferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT);
ThemeUtils.setAppNightMode(theme); ThemeUtils.setAppNightMode(theme, this);
restartActivitiesOnExit = true; restartActivitiesOnExit = true;
// recreate() could be used instead, but it doesn't have an animation B). // recreate() could be used instead, but it doesn't have an animation B).

View File

@ -1,48 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.util.NotificationHelper;
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/* Determine whether the user is currently logged in, and if so go ahead and load the
* timeline. Otherwise, start the activity_login screen. */
NotificationHelper.deleteLegacyNotificationChannels(this);
AccountEntity activeAccount = TuskyApplication.getInstance(this).getServiceLocator()
.get(AccountManager.class).getActiveAccount();
Intent intent;
if (activeAccount != null) {
intent = new Intent(this, MainActivity.class);
} else {
intent = LoginActivity.getIntent(this, false);
}
startActivity(intent);
finish();
}
}

View File

@ -0,0 +1,50 @@
/* Copyright 2018 Conny Duck
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NotificationHelper
import javax.inject.Inject
class SplashActivity : AppCompatActivity(), Injectable {
@Inject
lateinit var accountManager: AccountManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/** delete old notification channels that were in use in Tusky 1.4 */
NotificationHelper.deleteLegacyNotificationChannels(this)
/** Determine whether the user is currently logged in, and if so go ahead and load the
* timeline. Otherwise, start the activity_login screen. */
val intent = if (accountManager.activeAccount != null) {
Intent(this, MainActivity::class.java)
} else {
LoginActivity.getIntent(this, false)
}
startActivity(intent)
finish()
}
}

View File

@ -18,11 +18,9 @@ package com.keylesspalace.tusky;
import android.app.Activity; import android.app.Activity;
import android.app.Application; import android.app.Application;
import android.app.Service; import android.app.Service;
import android.app.UiModeManager;
import android.arch.persistence.room.Room; import android.arch.persistence.room.Room;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatDelegate; import android.support.v7.app.AppCompatDelegate;
@ -31,8 +29,6 @@ import com.jakewharton.picasso.OkHttp3Downloader;
import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.db.AppDatabase; import com.keylesspalace.tusky.db.AppDatabase;
import com.keylesspalace.tusky.di.AppInjector; import com.keylesspalace.tusky.di.AppInjector;
import com.keylesspalace.tusky.util.OkHttpUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
import javax.inject.Inject; import javax.inject.Inject;
@ -40,13 +36,11 @@ import javax.inject.Inject;
import dagger.android.AndroidInjector; import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector; import dagger.android.DispatchingAndroidInjector;
import dagger.android.HasActivityInjector; import dagger.android.HasActivityInjector;
import dagger.android.HasBroadcastReceiverInjector;
import dagger.android.HasServiceInjector; import dagger.android.HasServiceInjector;
import okhttp3.Cache;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
public class TuskyApplication extends Application implements HasActivityInjector, HasServiceInjector { public class TuskyApplication extends Application implements HasActivityInjector, HasServiceInjector, HasBroadcastReceiverInjector {
public static final String APP_THEME_DEFAULT = ThemeUtils.THEME_NIGHT;
private static AppDatabase db; private static AppDatabase db;
private AccountManager accountManager; private AccountManager accountManager;
@Inject @Inject
@ -54,23 +48,19 @@ public class TuskyApplication extends Application implements HasActivityInjector
@Inject @Inject
DispatchingAndroidInjector<Service> dispatchingServiceInjector; DispatchingAndroidInjector<Service> dispatchingServiceInjector;
@Inject @Inject
DispatchingAndroidInjector<BroadcastReceiver> dispatchingBroadcastReceiverInjector;
@Inject
NotificationPullJobCreator notificationPullJobCreator; NotificationPullJobCreator notificationPullJobCreator;
@Inject OkHttpClient okHttpClient;
public static AppDatabase getDB() { public static AppDatabase getDB() {
return db; return db;
} }
private static UiModeManager uiModeManager;
public static UiModeManager getUiModeManager() {
return uiModeManager;
}
public static TuskyApplication getInstance(@NonNull Context context) { public static TuskyApplication getInstance(@NonNull Context context) {
return (TuskyApplication) context.getApplicationContext(); return (TuskyApplication) context.getApplicationContext();
} }
private ServiceLocator serviceLocator; private ServiceLocator serviceLocator;
@Override @Override
@ -98,7 +88,6 @@ public class TuskyApplication extends Application implements HasActivityInjector
initPicasso(); initPicasso();
JobManager.create(this).addJobCreator(notificationPullJobCreator); JobManager.create(this).addJobCreator(notificationPullJobCreator);
uiModeManager = (UiModeManager) getSystemService(Context.UI_MODE_SERVICE);
//necessary for Android < APi 21 //necessary for Android < APi 21
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
@ -111,15 +100,7 @@ public class TuskyApplication extends Application implements HasActivityInjector
protected void initPicasso() { protected void initPicasso() {
// Initialize Picasso configuration // Initialize Picasso configuration
Picasso.Builder builder = new Picasso.Builder(this); Picasso.Builder builder = new Picasso.Builder(this);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); builder.downloader(new OkHttp3Downloader(okHttpClient));
OkHttpClient.Builder okHttpBuilder = OkHttpUtils.getCompatibleClientBuilder(preferences);
int cacheSize = 10*1024*1024; // 10 MiB
okHttpBuilder.cache(new Cache(getCacheDir(), cacheSize));
builder.downloader(new OkHttp3Downloader(okHttpBuilder.build()));
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
builder.listener((picasso, uri, exception) -> exception.printStackTrace()); builder.listener((picasso, uri, exception) -> exception.printStackTrace());
} }
@ -141,6 +122,11 @@ public class TuskyApplication extends Application implements HasActivityInjector
return dispatchingServiceInjector; return dispatchingServiceInjector;
} }
@Override
public AndroidInjector<BroadcastReceiver> broadcastReceiverInjector() {
return dispatchingBroadcastReceiverInjector;
}
public interface ServiceLocator { public interface ServiceLocator {
<T> T get(Class<T> clazz); <T> T get(Class<T> clazz);
} }

View File

@ -61,6 +61,13 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector() @ContributesAndroidInjector()
abstract fun contributesAboutActivity(): AboutActivity abstract fun contributesAboutActivity(): AboutActivity
@ContributesAndroidInjector()
abstract fun contributesLoginActivity(): LoginActivity
@ContributesAndroidInjector()
abstract fun contributesSplashActivity(): SplashActivity
@ContributesAndroidInjector @ContributesAndroidInjector
abstract fun contributesReportActivity(): ReportActivity abstract fun contributesReportActivity(): ReportActivity
} }

View File

@ -32,7 +32,8 @@ import javax.inject.Singleton
NetworkModule::class, NetworkModule::class,
AndroidInjectionModule::class, AndroidInjectionModule::class,
ActivitiesModule::class, ActivitiesModule::class,
ServicesModule::class ServicesModule::class,
BroadcastReceiverModule::class
]) ])
interface AppComponent { interface AppComponent {
@Component.Builder @Component.Builder

View File

@ -0,0 +1,26 @@
/* Copyright 2018 Conny Duck
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class BroadcastReceiverModule {
@ContributesAndroidInjector
abstract fun contributeNotificationClearBroadcastReceiver() : NotificationClearBroadcastReceiver
}

View File

@ -16,11 +16,12 @@
package com.keylesspalace.tusky.di package com.keylesspalace.tusky.di
import android.content.SharedPreferences import android.content.Context
import android.text.Spanned import android.text.Spanned
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer import com.google.gson.JsonDeserializer
import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.json.SpannedTypeAdapter
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
@ -31,8 +32,8 @@ import dagger.Provides
import dagger.multibindings.ClassKey import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap
import dagger.multibindings.IntoSet import dagger.multibindings.IntoSet
import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter import retrofit2.Converter
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
@ -67,32 +68,24 @@ class NetworkModule {
fun providesConverterFactory(gson: Gson): Converter.Factory = GsonConverterFactory.create(gson) fun providesConverterFactory(gson: Gson): Converter.Factory = GsonConverterFactory.create(gson)
@Provides @Provides
@IntoSet
@Singleton @Singleton
fun providesAuthInterceptor(accountManager: AccountManager): Interceptor { fun providesHttpClient(accountManager: AccountManager,
// should accept AccountManager here probably but I don't want to break things yet context: Context): OkHttpClient {
return InstanceSwitchAuthInterceptor(accountManager) return OkHttpUtils.getCompatibleClientBuilder(context)
}
@Provides
@Singleton
fun providesHttpClient(interceptors: @JvmSuppressWildcards Set<Interceptor>,
preferences: SharedPreferences): OkHttpClient {
return OkHttpUtils.getCompatibleClientBuilder(preferences)
.apply { .apply {
interceptors.fold(this) { b, i -> addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
b.addInterceptor(i) if (BuildConfig.DEBUG) {
addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))
} }
} }
.build() .build()
} }
@Provides @Provides
@Singleton @Singleton
fun providesRetrofit(httpClient: OkHttpClient, fun providesRetrofit(httpClient: OkHttpClient,
converters: @JvmSuppressWildcards Set<Converter.Factory>): Retrofit { converters: @JvmSuppressWildcards Set<Converter.Factory>): Retrofit {
return Retrofit.Builder().baseUrl("https://dummy.placeholder/") return Retrofit.Builder().baseUrl("https://"+MastodonApi.PLACEHOLDER_DOMAIN)
.client(httpClient) .client(httpClient)
.let { builder -> .let { builder ->
// Doing it this way in case builder will be immutable so we return the final // Doing it this way in case builder will be immutable so we return the final

View File

@ -38,7 +38,6 @@ import android.view.ViewGroup;
import com.keylesspalace.tusky.MainActivity; import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.TuskyApplication;
import com.keylesspalace.tusky.adapter.FooterViewHolder; import com.keylesspalace.tusky.adapter.FooterViewHolder;
import com.keylesspalace.tusky.adapter.NotificationsAdapter; import com.keylesspalace.tusky.adapter.NotificationsAdapter;
import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountEntity;
@ -108,6 +107,8 @@ public class NotificationsFragment extends SFragment implements
public TimelineCases timelineCases; public TimelineCases timelineCases;
@Inject @Inject
public MastodonApi mastodonApi; public MastodonApi mastodonApi;
@Inject
AccountManager accountManager;
private SwipeRefreshLayout swipeRefreshLayout; private SwipeRefreshLayout swipeRefreshLayout;
private LinearLayoutManager layoutManager; private LinearLayoutManager layoutManager;
@ -606,8 +607,7 @@ public class NotificationsFragment extends SFragment implements
} }
private void saveNewestNotificationId(List<Notification> notifications) { private void saveNewestNotificationId(List<Notification> notifications) {
AccountManager accountManager = TuskyApplication.getInstance(getContext())
.getServiceLocator().get(AccountManager.class);
AccountEntity account = accountManager.getActiveAccount(); AccountEntity account = accountManager.getActiveAccount();
BigInteger lastNoti = new BigInteger(account.getLastNotificationId()); BigInteger lastNoti = new BigInteger(account.getLastNotificationId());

View File

@ -42,28 +42,35 @@ public final class InstanceSwitchAuthInterceptor implements Interceptor {
public Response intercept(@NonNull Chain chain) throws IOException { public Response intercept(@NonNull Chain chain) throws IOException {
Request originalRequest = chain.request(); Request originalRequest = chain.request();
AccountEntity currentAccount = accountManager.getActiveAccount();
Request.Builder builder = originalRequest.newBuilder(); // only switch domains if the request comes from retrofit
if (originalRequest.url().host().equals(MastodonApi.PLACEHOLDER_DOMAIN)) {
AccountEntity currentAccount = accountManager.getActiveAccount();
String instanceHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER); Request.Builder builder = originalRequest.newBuilder();
if (instanceHeader != null) {
// use domain explicitly specified in custom header String instanceHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER);
builder.url(swapHost(originalRequest.url(), instanceHeader)); if (instanceHeader != null) {
builder.removeHeader(MastodonApi.DOMAIN_HEADER); // use domain explicitly specified in custom header
} else if (currentAccount != null) { builder.url(swapHost(originalRequest.url(), instanceHeader));
//use domain of current account builder.removeHeader(MastodonApi.DOMAIN_HEADER);
builder.url(swapHost(originalRequest.url(), currentAccount.getDomain())) } else if (currentAccount != null) {
.header("Authorization", //use domain of current account
String.format("Bearer %s", currentAccount.getAccessToken())); builder.url(swapHost(originalRequest.url(), currentAccount.getDomain()))
.header("Authorization",
String.format("Bearer %s", currentAccount.getAccessToken()));
}
Request newRequest = builder.build();
return chain.proceed(newRequest);
} else {
return chain.proceed(originalRequest);
} }
Request newRequest = builder.build();
return chain.proceed(newRequest);
} }
@NonNull @NonNull
private HttpUrl swapHost(@NonNull HttpUrl url, @NonNull String host) { private static HttpUrl swapHost(@NonNull HttpUrl url, @NonNull String host) {
return url.newBuilder().host(host).build(); return url.newBuilder().host(host).build();
} }
} }

View File

@ -53,6 +53,7 @@ import retrofit2.http.Query;
public interface MastodonApi { public interface MastodonApi {
String ENDPOINT_AUTHORIZE = "/oauth/authorize"; String ENDPOINT_AUTHORIZE = "/oauth/authorize";
String DOMAIN_HEADER = "domain"; String DOMAIN_HEADER = "domain";
String PLACEHOLDER_DOMAIN = "dummy.placeholder";
@GET("api/v1/timelines/home") @GET("api/v1/timelines/home")
Call<List<Status>> homeTimeline( Call<List<Status>> homeTimeline(
@ -246,6 +247,7 @@ public interface MastodonApi {
@FormUrlEncoded @FormUrlEncoded
@POST("api/v1/apps") @POST("api/v1/apps")
Call<AppCredentials> authenticateApp( Call<AppCredentials> authenticateApp(
@Header(DOMAIN_HEADER) String domain,
@Field("client_name") String clientName, @Field("client_name") String clientName,
@Field("redirect_uris") String redirectUris, @Field("redirect_uris") String redirectUris,
@Field("scopes") String scopes, @Field("scopes") String scopes,
@ -254,6 +256,7 @@ public interface MastodonApi {
@FormUrlEncoded @FormUrlEncoded
@POST("oauth/token") @POST("oauth/token")
Call<AccessToken> fetchOAuthToken( Call<AccessToken> fetchOAuthToken(
@Header(DOMAIN_HEADER) String domain,
@Field("client_id") String clientId, @Field("client_id") String clientId,
@Field("client_secret") String clientSecret, @Field("client_secret") String clientSecret,
@Field("redirect_uri") String redirectUri, @Field("redirect_uri") String redirectUri,

View File

@ -19,17 +19,21 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.util.NotificationHelper import com.keylesspalace.tusky.util.NotificationHelper
import dagger.android.AndroidInjection
import javax.inject.Inject
class NotificationClearBroadcastReceiver : BroadcastReceiver() { class NotificationClearBroadcastReceiver : BroadcastReceiver() {
@Inject
lateinit var accountManager: AccountManager
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
AndroidInjection.inject(this, context)
val accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1) val accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1)
val accountManager = TuskyApplication.getInstance(context)
.serviceLocator.get(AccountManager::class.java)
val account = accountManager.getAccountById(accountId) val account = accountManager.getAccountById(accountId)
if (account != null) { if (account != null) {
account.activeNotifications = "[]" account.activeNotifications = "[]"

View File

@ -15,8 +15,10 @@
package com.keylesspalace.tusky.util; package com.keylesspalace.tusky.util;
import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.Log; import android.util.Log;
@ -43,11 +45,11 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager; import javax.net.ssl.X509TrustManager;
import okhttp3.Cache;
import okhttp3.ConnectionSpec; import okhttp3.ConnectionSpec;
import okhttp3.Interceptor; import okhttp3.Interceptor;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.logging.HttpLoggingInterceptor;
public class OkHttpUtils { public class OkHttpUtils {
private static final String TAG = "OkHttpUtils"; // logging tag private static final String TAG = "OkHttpUtils"; // logging tag
@ -65,7 +67,11 @@ public class OkHttpUtils {
* TLS 1.1 and 1.2 have to be manually enabled on API levels 16-20. * TLS 1.1 and 1.2 have to be manually enabled on API levels 16-20.
*/ */
@NonNull @NonNull
public static OkHttpClient.Builder getCompatibleClientBuilder(SharedPreferences preferences) { public static OkHttpClient.Builder getCompatibleClientBuilder(@NonNull Context context) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
boolean httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false); boolean httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false);
String httpServer = preferences.getString("httpProxyServer", ""); String httpServer = preferences.getString("httpProxyServer", "");
int httpPort = Integer.parseInt(preferences.getString("httpProxyPort", "-1")); int httpPort = Integer.parseInt(preferences.getString("httpProxyPort", "-1"));
@ -81,10 +87,13 @@ public class OkHttpUtils {
specList.add(fallback); specList.add(fallback);
specList.add(ConnectionSpec.CLEARTEXT); specList.add(ConnectionSpec.CLEARTEXT);
int cacheSize = 10*1024*1024; // 10 MiB
OkHttpClient.Builder builder = new OkHttpClient.Builder() OkHttpClient.Builder builder = new OkHttpClient.Builder()
.addInterceptor(getUserAgentInterceptor()) .addInterceptor(getUserAgentInterceptor())
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS)
.cache(new Cache(context.getCacheDir(), cacheSize))
.connectionSpecs(specList); .connectionSpecs(specList);
if (httpProxyEnabled && !httpServer.isEmpty() && (httpPort > 0) && (httpPort < 65535)) { if (httpProxyEnabled && !httpServer.isEmpty() && (httpPort > 0) && (httpPort < 65535)) {
@ -92,18 +101,9 @@ public class OkHttpUtils {
builder.proxy(new Proxy(Proxy.Type.HTTP, address)); builder.proxy(new Proxy(Proxy.Type.HTTP, address));
} }
if(BuildConfig.DEBUG) {
builder.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC));
}
return enableHigherTlsOnPreLollipop(builder); return enableHigherTlsOnPreLollipop(builder);
} }
@NonNull
public static OkHttpClient getCompatibleClient(SharedPreferences preferences) {
return getCompatibleClientBuilder(preferences).build();
}
/** /**
* Add a custom User-Agent that contains Tusky & Android Version to all requests * Add a custom User-Agent that contains Tusky & Android Version to all requests
* Example: * Example:

View File

@ -30,13 +30,13 @@ import android.support.v7.app.AppCompatDelegate;
import android.util.TypedValue; import android.util.TypedValue;
import android.widget.ImageView; import android.widget.ImageView;
import com.keylesspalace.tusky.TuskyApplication;
/** /**
* Provides runtime compatibility to obtain theme information and re-theme views, especially where * Provides runtime compatibility to obtain theme information and re-theme views, especially where
* the ability to do so is not supported in resource files. * the ability to do so is not supported in resource files.
*/ */
public class ThemeUtils { public class ThemeUtils {
public static final String APP_THEME_DEFAULT = ThemeUtils.THEME_NIGHT;
public static final String THEME_NIGHT = "night"; public static final String THEME_NIGHT = "night";
public static final String THEME_DAY = "day"; public static final String THEME_DAY = "day";
public static final String THEME_AUTO = "auto"; public static final String THEME_AUTO = "auto";
@ -91,7 +91,7 @@ public class ThemeUtils {
drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN); drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN);
} }
public static void setAppNightMode(String flavor) { public static void setAppNightMode(String flavor, Context context) {
int mode; int mode;
switch (flavor) { switch (flavor) {
default: default:
@ -107,7 +107,8 @@ public class ThemeUtils {
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
TuskyApplication.getUiModeManager().setNightMode(mode); UiModeManager uiModeManager = (UiModeManager)context.getApplicationContext().getSystemService(Context.UI_MODE_SERVICE);
uiModeManager.setNightMode(mode);
} else { } else {
AppCompatDelegate.setDefaultNightMode(mode); AppCompatDelegate.setDefaultNightMode(mode);
} }