Add Dagger (#554)

* Add Dagger DI

* Preemptively fix tests

* Add missing licenses

* DI fixes

* ci fixes
This commit is contained in:
Ivan Kupalov 2018-03-27 20:47:00 +03:00 committed by Konrad Pozniak
parent f5958cd28d
commit f43ef319d0
41 changed files with 1040 additions and 415 deletions

View File

@ -44,6 +44,7 @@ android {
}
ext.supportLibraryVersion = '27.1.0'
ext.daggerVersion = '2.15'
dependencies {
implementation('com.mikepenz:materialdrawer:6.0.6@aar') {
@ -77,6 +78,13 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
testImplementation 'junit:junit:4.12'
implementation "com.google.dagger:dagger:$daggerVersion"
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
implementation "com.google.dagger:dagger-android:$daggerVersion"
implementation "com.google.dagger:dagger-android-support:$daggerVersion"
kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
testImplementation "org.robolectric:robolectric:3.7.1"
testCompile "org.mockito:mockito-inline:2.15.0"
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {

View File

@ -64,3 +64,5 @@
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
-dontwarn com.google.errorprone.annotations.*

View File

@ -2,6 +2,7 @@ package com.keylesspalace.tusky;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
@ -10,17 +11,24 @@ import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.network.MastodonApi;
import java.util.List;
import javax.inject.Inject;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class AboutActivity extends BaseActivity {
public class AboutActivity extends BaseActivity implements Injectable {
private Button appAccountButton;
@Inject
public MastodonApi mastodonApi;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -42,12 +50,7 @@ public class AboutActivity extends BaseActivity {
versionTextView.setText(String.format(versionFormat, versionName));
appAccountButton = findViewById(R.id.tusky_profile_button);
appAccountButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onAccountButtonClick();
}
});
appAccountButton.setOnClickListener(v -> onAccountButtonClick());
}
private void onAccountButtonClick() {
@ -68,10 +71,10 @@ public class AboutActivity extends BaseActivity {
private void searchForAccountThenViewIt() {
Callback<List<Account>> callback = new Callback<List<Account>>() {
@Override
public void onResponse(Call<List<Account>> call, Response<List<Account>> response) {
public void onResponse(@NonNull Call<List<Account>> call, @NonNull Response<List<Account>> response) {
if (response.isSuccessful()) {
List<Account> accountList = response.body();
if (!accountList.isEmpty()) {
if (accountList != null && !accountList.isEmpty()) {
String id = accountList.get(0).getId();
getPrivatePreferences().edit()
.putString("appAccountId", id)
@ -86,7 +89,7 @@ public class AboutActivity extends BaseActivity {
}
@Override
public void onFailure(Call<List<Account>> call, Throwable t) {
public void onFailure(@NonNull Call<List<Account>> call, @NonNull Throwable t) {
onSearchFailed();
}
};

View File

@ -31,6 +31,7 @@ import android.support.design.widget.CollapsingToolbarLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager;
@ -51,6 +52,7 @@ import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.pager.AccountPagerAdapter;
import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.Assert;
@ -64,11 +66,17 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.support.HasSupportFragmentInjector;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public final class AccountActivity extends BaseActivity implements ActionButtonActivity {
public final class AccountActivity extends BaseActivity implements ActionButtonActivity,
HasSupportFragmentInjector {
private static final String TAG = "AccountActivity"; // logging tag
private enum FollowState {
@ -77,6 +85,11 @@ public final class AccountActivity extends BaseActivity implements ActionButtonA
REQUESTED,
}
@Inject
public MastodonApi mastodonApi;
@Inject
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
private String accountId;
private FollowState followState;
private boolean blocking;
@ -690,4 +703,8 @@ public final class AccountActivity extends BaseActivity implements ActionButtonA
return null;
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return dispatchingAndroidInjector;
}
}

View File

@ -20,6 +20,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
@ -27,7 +28,16 @@ import android.view.MenuItem;
import com.keylesspalace.tusky.fragment.AccountListFragment;
public final class AccountListActivity extends BaseActivity {
import javax.inject.Inject;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.support.HasSupportFragmentInjector;
public final class AccountListActivity extends BaseActivity implements HasSupportFragmentInjector {
@Inject
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
private static final String TYPE_EXTRA = "type";
private static final String ARG_EXTRA = "arg";
@ -131,4 +141,9 @@ public final class AccountListActivity extends BaseActivity {
}
return super.onOptionsItemSelected(item);
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return dispatchingAndroidInjector;
}
}

View File

@ -25,41 +25,21 @@ import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.text.Spanned;
import android.util.TypedValue;
import android.view.Menu;
import com.evernote.android.job.JobManager;
import com.evernote.android.job.JobRequest;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.json.SpannedTypeAdapter;
import com.keylesspalace.tusky.network.AuthInterceptor;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.OkHttpUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public abstract class BaseActivity extends AppCompatActivity {
public MastodonApi mastodonApi;
protected Dispatcher mastodonApiDispatcher;
private AccountManager accountManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
accountManager = TuskyApplication.getInstance(this).getServiceLocator()
.get(AccountManager.class);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
/* There isn't presently a way to globally change the theme of a whole application at
@ -84,19 +64,7 @@ public abstract class BaseActivity extends AppCompatActivity {
}
getTheme().applyStyle(style, false);
if (redirectIfNotLoggedIn()) {
return;
}
createMastodonApi();
}
@Override
protected void onDestroy() {
if (mastodonApiDispatcher != null) {
mastodonApiDispatcher.cancelAll();
}
super.onDestroy();
redirectIfNotLoggedIn();
}
@Override
@ -123,44 +91,13 @@ public abstract class BaseActivity extends AppCompatActivity {
return getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
}
protected String getBaseUrl() {
AccountEntity account = accountManager.getActiveAccount();
if (account != null) {
return "https://" + account.getDomain();
} else {
return "";
}
}
protected void createMastodonApi() {
mastodonApiDispatcher = new Dispatcher();
Gson gson = new GsonBuilder()
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
.create();
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
OkHttpClient.Builder okBuilder =
OkHttpUtils.getCompatibleClientBuilder(preferences)
.addInterceptor(new AuthInterceptor(accountManager))
.dispatcher(mastodonApiDispatcher);
if (BuildConfig.DEBUG) {
okBuilder.addInterceptor(
new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC));
}
Retrofit retrofit = new Retrofit.Builder().baseUrl(getBaseUrl())
.client(okBuilder.build())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
mastodonApi = retrofit.create(MastodonApi.class);
}
protected boolean redirectIfNotLoggedIn() {
if (accountManager.getActiveAccount() == null) {
// This is very ugly but we cannot inject into parent class and injecting into every
// subclass seems inconvenient as well.
AccountEntity account = ((TuskyApplication) getApplicationContext())
.getServiceLocator().get(AccountManager.class)
.getActiveAccount();
if (account == null) {
Intent intent = new Intent(this, LoginActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

View File

@ -82,10 +82,12 @@ import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.db.TootDao;
import com.keylesspalace.tusky.db.TootEntity;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.fragment.ComposeOptionsFragment;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.ProgressRequestBody;
import com.keylesspalace.tusky.util.CountUpDownLatch;
import com.keylesspalace.tusky.util.DownsizeImageTask;
@ -115,6 +117,8 @@ import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import retrofit2.Call;
@ -122,7 +126,9 @@ import retrofit2.Callback;
import retrofit2.Response;
public final class ComposeActivity extends BaseActivity
implements ComposeOptionsFragment.Listener, MentionAutoCompleteAdapter.AccountSearchProvider {
implements ComposeOptionsFragment.Listener,
MentionAutoCompleteAdapter.AccountSearchProvider,
Injectable {
private static final String TAG = "ComposeActivity"; // logging tag
private static final int STATUS_CHARACTER_LIMIT = 500;
private static final int STATUS_MEDIA_SIZE_LIMIT = 8388608; // 8MiB
@ -145,6 +151,11 @@ public final class ComposeActivity extends BaseActivity
private static TootDao tootDao = TuskyApplication.getDB().tootDao();
@Inject
public MastodonApi mastodonApi;
@Inject
public AccountManager accountManager;
private TextView replyTextView;
private TextView replyContentTextView;
private EditTextTyped textEditor;
@ -206,11 +217,9 @@ public final class ComposeActivity extends BaseActivity
}
// setup the account image
AccountEntity activeAccount = TuskyApplication.getInstance(this).getServiceLocator()
.get(AccountManager.class).getActiveAccount();
final AccountEntity activeAccount = accountManager.getActiveAccount();
if (activeAccount != null) {
ImageView composeAvatar = findViewById(R.id.composeAvatar);
if (TextUtils.isEmpty(activeAccount.getProfilePictureUrl())) {

View File

@ -32,7 +32,9 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.IOUtils
import com.squareup.picasso.Picasso
import com.theartofdev.edmodo.cropper.CropImage
@ -46,6 +48,7 @@ import retrofit2.Callback
import retrofit2.Response
import java.io.*
import java.util.*
import javax.inject.Inject
private const val TAG = "EditProfileActivity"
@ -66,7 +69,7 @@ private const val AVATAR_SIZE = 120
private const val HEADER_WIDTH = 700
private const val HEADER_HEIGHT = 335
class EditProfileActivity : BaseActivity() {
class EditProfileActivity : BaseActivity(), Injectable {
private var oldDisplayName: String? = null
private var oldNote: String? = null
@ -75,6 +78,9 @@ class EditProfileActivity : BaseActivity() {
private var avatarChanged: Boolean = false
private var headerChanged: Boolean = false
@Inject
lateinit var mastodonApi: MastodonApi
private enum class PickType {
NOTHING,
AVATAR,
@ -100,11 +106,11 @@ class EditProfileActivity : BaseActivity() {
avatarChanged = it.getBoolean(KEY_AVATAR_CHANGED)
headerChanged = it.getBoolean(KEY_HEADER_CHANGED)
if(avatarChanged) {
if (avatarChanged) {
val avatar = BitmapFactory.decodeFile(getCacheFileForName(AVATAR_FILE_NAME).absolutePath)
avatarPreview.setImageBitmap(avatar)
}
if(headerChanged) {
if (headerChanged) {
val header = BitmapFactory.decodeFile(getCacheFileForName(HEADER_FILE_NAME).absolutePath)
headerPreview.setImageBitmap(header)
}
@ -135,13 +141,13 @@ class EditProfileActivity : BaseActivity() {
displayNameEditText.setText(oldDisplayName)
noteEditText.setText(oldNote)
if(!avatarChanged) {
if (!avatarChanged) {
Picasso.with(avatarPreview.context)
.load(me.avatar)
.placeholder(R.drawable.avatar_default)
.into(avatarPreview)
}
if(!headerChanged) {
if (!headerChanged) {
Picasso.with(headerPreview.context)
.load(me.header)
.placeholder(R.drawable.account_header_default)
@ -253,21 +259,21 @@ class EditProfileActivity : BaseActivity() {
RequestBody.create(MultipartBody.FORM, newNote)
}
val avatar = if(avatarChanged) {
val avatar = if (avatarChanged) {
val avatarBody = RequestBody.create(MediaType.parse("image/png"), getCacheFileForName(AVATAR_FILE_NAME))
MultipartBody.Part.createFormData("avatar", getFileName(), avatarBody)
} else {
null
}
val header = if(headerChanged) {
val header = if (headerChanged) {
val headerBody = RequestBody.create(MediaType.parse("image/png"), getCacheFileForName(HEADER_FILE_NAME))
MultipartBody.Part.createFormData("header", getFileName(), headerBody)
} else {
null
}
if(displayName == null && note == null && avatar == null && header == null) {
if (displayName == null && note == null && avatar == null && header == null) {
/** if nothing has changed, there is no need to make a network request */
finish()
return
@ -413,11 +419,11 @@ class EditProfileActivity : BaseActivity() {
return java.lang.Long.toHexString(Random().nextLong())
}
private class ResizeImageTask (private val contentResolver: ContentResolver,
private val resizeWidth: Int,
private val resizeHeight: Int,
private val cacheFile: File,
private val listener: Listener) : AsyncTask<Uri, Void, Boolean>() {
private class ResizeImageTask(private val contentResolver: ContentResolver,
private val resizeWidth: Int,
private val resizeHeight: Int,
private val cacheFile: File,
private val listener: Listener) : AsyncTask<Uri, Void, Boolean>() {
private var resultBitmap: Bitmap? = null
override fun doInBackground(vararg uris: Uri): Boolean? {
@ -445,7 +451,7 @@ class EditProfileActivity : BaseActivity() {
//dont upscale image if its smaller than the desired size
val bitmap =
if(sourceBitmap.width <= resizeWidth && sourceBitmap.height <= resizeHeight) {
if (sourceBitmap.width <= resizeWidth && sourceBitmap.height <= resizeHeight) {
sourceBitmap
} else {
Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight, true)

View File

@ -25,7 +25,17 @@ import android.view.MenuItem;
import com.keylesspalace.tusky.fragment.TimelineFragment;
public class FavouritesActivity extends BaseActivity {
import javax.inject.Inject;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.support.HasSupportFragmentInjector;
public class FavouritesActivity extends BaseActivity implements HasSupportFragmentInjector {
@Inject
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -56,4 +66,9 @@ public class FavouritesActivity extends BaseActivity {
}
return super.onOptionsItemSelected(item);
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return dispatchingAndroidInjector;
}
}

View File

@ -12,19 +12,19 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.ThemeUtils
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.IconicsDrawable
import com.varunest.sparkbutton.helpers.Utils
import retrofit2.Call
import retrofit2.Response
import java.lang.ref.WeakReference
import javax.inject.Inject
/**
* Created by charlag on 1/4/18.
@ -83,7 +83,7 @@ class ListsViewModel(private val api: MastodonApi) {
}
}
class ListsActivity : BaseActivity(), ListsView {
class ListsActivity : BaseActivity(), ListsView, Injectable {
companion object {
@JvmStatic
@ -92,6 +92,9 @@ class ListsActivity : BaseActivity(), ListsView {
}
}
@Inject
lateinit var mastodonApi: MastodonApi
private lateinit var recyclerView: RecyclerView
private lateinit var progressBar: ProgressBar

View File

@ -27,6 +27,7 @@ import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.TabLayout;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewPager;
@ -40,6 +41,7 @@ import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.pager.TimelinePagerAdapter;
import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.NotificationHelper;
@ -63,11 +65,18 @@ import com.squareup.picasso.Picasso;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.support.AndroidSupportInjection;
import dagger.android.support.HasSupportFragmentInjector;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainActivity extends BaseActivity implements ActionButtonActivity {
public class MainActivity extends BaseActivity implements ActionButtonActivity,
HasSupportFragmentInjector {
private static final String TAG = "MainActivity"; // logging tag
private static final long DRAWER_ITEM_ADD_ACCOUNT = -13;
private static final long DRAWER_ITEM_EDIT_PROFILE = 0;
@ -82,6 +91,11 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
private static final long DRAWER_ITEM_SAVED_TOOT = 9;
private static final long DRAWER_ITEM_LISTS = 10;
@Inject
public MastodonApi mastodonApi;
@Inject
public DispatchingAndroidInjector<Fragment> fragmentInjector;
private static int COMPOSE_RESULT = 1;
AccountManager accountManager;
@ -93,10 +107,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// account switching has to be done before MastodonApi is created in super.onCreate
Intent intent = getIntent();
int tabPosition = 0;
accountManager = TuskyApplication.getInstance(this).getServiceLocator()
@ -550,4 +561,9 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
public FloatingActionButton getActionButton() {
return composeButton;
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return fragmentInjector;
}
}

View File

@ -4,19 +4,29 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.design.widget.FloatingActionButton
import android.support.v4.app.Fragment
import android.support.v7.widget.Toolbar
import android.view.MenuItem
import android.widget.FrameLayout
import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import javax.inject.Inject
class ModalTimelineActivity : BaseActivity(), ActionButtonActivity, HasSupportFragmentInjector {
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
class ModalTimelineActivity : BaseActivity(), ActionButtonActivity {
companion object {
private const val ARG_KIND = "kind"
private const val ARG_ARG = "arg"
@JvmStatic fun newIntent(context: Context, kind: TimelineFragment.Kind,
argument: String?): Intent {
@JvmStatic
fun newIntent(context: Context, kind: TimelineFragment.Kind,
argument: String?): Intent {
val intent = Intent(context, ModalTimelineActivity::class.java)
intent.putExtra(ARG_KIND, kind)
intent.putExtra(ARG_ARG, argument)
@ -24,6 +34,7 @@ class ModalTimelineActivity : BaseActivity(), ActionButtonActivity {
}
}
lateinit var contentFrame: FrameLayout
override fun onCreate(savedInstanceState: Bundle?) {
@ -41,8 +52,8 @@ class ModalTimelineActivity : BaseActivity(), ActionButtonActivity {
}
if (supportFragmentManager.findFragmentById(R.id.content_frame) == null) {
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineFragment.Kind ?:
TimelineFragment.Kind.HOME
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineFragment.Kind
?: TimelineFragment.Kind.HOME
val argument = intent?.getStringExtra(ARG_ARG)
supportFragmentManager.beginTransaction()
.replace(R.id.content_frame, TimelineFragment.newInstance(kind, argument))
@ -60,4 +71,8 @@ class ModalTimelineActivity : BaseActivity(), ActionButtonActivity {
}
return false
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return dispatchingAndroidInjector
}
}

View File

@ -16,23 +16,17 @@
package com.keylesspalace.tusky;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spanned;
import android.util.Log;
import com.evernote.android.job.Job;
import com.evernote.android.job.JobCreator;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.json.SpannedTypeAdapter;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.NotificationHelper;
import com.keylesspalace.tusky.util.OkHttpUtils;
import java.io.IOException;
import java.math.BigInteger;
@ -40,10 +34,9 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import okhttp3.OkHttpClient;
import javax.inject.Inject;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
/**
* Created by charlag on 31/10/17.
@ -55,65 +48,53 @@ public final class NotificationPullJobCreator implements JobCreator {
static final String NOTIFICATIONS_JOB_TAG = "notifications_job_tag";
private Context context;
private final MastodonApi api;
private final Context context;
private final AccountManager accountManager;
NotificationPullJobCreator(Context context) {
@Inject NotificationPullJobCreator(MastodonApi api, Context context,
AccountManager accountManager) {
this.api = api;
this.context = context;
this.accountManager = accountManager;
}
@Nullable
@Override
public Job create(@NonNull String tag) {
if (tag.equals(NOTIFICATIONS_JOB_TAG)) {
return new NotificationPullJob(context);
return new NotificationPullJob(context, accountManager, api);
}
return null;
}
private static MastodonApi createMastodonApi(String domain, Context context) {
SharedPreferences preferences = context.getSharedPreferences(
context.getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder(preferences)
.build();
Gson gson = new GsonBuilder()
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + domain)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
return retrofit.create(MastodonApi.class);
}
private final static class NotificationPullJob extends Job {
private Context context;
private final Context context;
private final AccountManager accountManager;
private final MastodonApi mastodonApi;
NotificationPullJob(Context context) {
NotificationPullJob(Context context, AccountManager accountManager,
MastodonApi mastodonApi) {
this.context = context;
this.accountManager = accountManager;
this.mastodonApi = mastodonApi;
}
@NonNull
@Override
protected Result onRunJob(@NonNull Params params) {
AccountManager accountManager = TuskyApplication.getInstance(context).getServiceLocator()
.get(AccountManager.class);
List<AccountEntity> accountList = new ArrayList<>(accountManager.getAllAccountsOrderedByActive());
for (AccountEntity account : accountList) {
if (account.getNotificationsEnabled()) {
MastodonApi api = createMastodonApi(account.getDomain(), context);
try {
Log.d(TAG, "getting Notifications for " + account.getFullName());
Response<List<Notification>> notifications =
api.notificationsWithAuth(String.format("Bearer %s", account.getAccessToken())).execute();
mastodonApi.notificationsWithAuth(
String.format("Bearer %s", account.getAccessToken()),
account.getDomain()
)
.execute();
if (notifications.isSuccessful()) {
onNotificationsReceived(account, notifications.body());
} else {
@ -127,22 +108,15 @@ public final class NotificationPullJobCreator implements JobCreator {
}
return Result.SUCCESS;
}
private void onNotificationsReceived(AccountEntity account, List<Notification> notificationList) {
Collections.reverse(notificationList);
BigInteger newId = new BigInteger(account.getLastNotificationId());
BigInteger newestId = BigInteger.ZERO;
for (Notification notification : notificationList) {
BigInteger currentId = new BigInteger(notification.getId());
if (isBiggerThan(currentId, newestId)) {
newestId = currentId;
}
@ -153,12 +127,10 @@ public final class NotificationPullJobCreator implements JobCreator {
}
account.setLastNotificationId(newestId.toString());
TuskyApplication.getInstance(context).getServiceLocator()
.get(AccountManager.class).saveAccount(account);
accountManager.saveAccount(account);
}
private boolean isBiggerThan(BigInteger newId, BigInteger lastShownNotificationId) {
return lastShownNotificationId.compareTo(newId) == -1;
}
}

View File

@ -32,7 +32,9 @@ import android.view.View;
import android.widget.EditText;
import com.keylesspalace.tusky.adapter.ReportAdapter;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.HtmlUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
@ -40,14 +42,19 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class ReportActivity extends BaseActivity {
public class ReportActivity extends BaseActivity implements Injectable {
private static final String TAG = "ReportActivity"; // logging tag
@Inject
public MastodonApi mastodonApi;
private View anyView; // what Snackbar will use to find the root view
private ReportAdapter adapter;
private boolean reportAlreadyInFlight;
@ -118,7 +125,7 @@ public class ReportActivity extends BaseActivity {
}
private void sendReport(final String accountId, final String[] statusIds,
final String comment) {
final String comment) {
Callback<ResponseBody> callback = new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
@ -145,7 +152,7 @@ public class ReportActivity extends BaseActivity {
}
private void onSendFailure(final String accountId, final String[] statusIds,
final String comment) {
final String comment) {
Snackbar.make(anyView, R.string.error_generic, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry, new View.OnClickListener() {
@Override

View File

@ -35,17 +35,24 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import com.keylesspalace.tusky.adapter.SearchResultsAdapter;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.SearchResults;
import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.network.MastodonApi;
import javax.inject.Inject;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class SearchActivity extends BaseActivity implements SearchView.OnQueryTextListener,
LinkListener {
LinkListener, Injectable {
private static final String TAG = "SearchActivity"; // logging tag
@Inject
public MastodonApi mastodonApi;
private ProgressBar progressBar;
private TextView messageNoResults;
private SearchResultsAdapter adapter;

View File

@ -15,6 +15,7 @@
package com.keylesspalace.tusky;
import android.app.Activity;
import android.app.Application;
import android.app.UiModeManager;
import android.arch.persistence.room.Room;
@ -28,15 +29,26 @@ import com.evernote.android.job.JobManager;
import com.jakewharton.picasso.OkHttp3Downloader;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.db.AppDatabase;
import com.keylesspalace.tusky.di.AppInjector;
import com.keylesspalace.tusky.util.OkHttpUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.squareup.picasso.Picasso;
public class TuskyApplication extends Application {
import javax.inject.Inject;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.HasActivityInjector;
public class TuskyApplication extends Application implements HasActivityInjector {
public static final String APP_THEME_DEFAULT = ThemeUtils.THEME_NIGHT;
private static AppDatabase db;
private AccountManager accountManager;
@Inject
DispatchingAndroidInjector<Activity> dispatchingAndroidInjector;
@Inject
NotificationPullJobCreator notificationPullJobCreator;
public static AppDatabase getDB() {
return db;
@ -58,8 +70,12 @@ public class TuskyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initPicasso();
db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "tuskyDB")
.allowMainThreadQueries()
.addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5)
.build();
accountManager = new AccountManager(db);
serviceLocator = new ServiceLocator() {
@Override
public <T> T get(Class<T> clazz) {
@ -72,19 +88,14 @@ public class TuskyApplication extends Application {
}
};
db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "tuskyDB")
.allowMainThreadQueries()
.addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5)
.build();
JobManager.create(this).addJobCreator(new NotificationPullJobCreator(this));
AppInjector.INSTANCE.init(this);
initPicasso();
JobManager.create(this).addJobCreator(notificationPullJobCreator);
uiModeManager = (UiModeManager) getSystemService(Context.UI_MODE_SERVICE);
//necessary for Android < APi 21
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
accountManager = new AccountManager();
}
protected void initPicasso() {
@ -106,6 +117,11 @@ public class TuskyApplication extends Application {
return serviceLocator;
}
@Override
public AndroidInjector<Activity> activityInjector() {
return dispatchingAndroidInjector;
}
public interface ServiceLocator {
<T> T get(Class<T> clazz);
}

View File

@ -25,7 +25,17 @@ import android.view.MenuItem;
import com.keylesspalace.tusky.fragment.TimelineFragment;
public class ViewTagActivity extends BaseActivity {
import javax.inject.Inject;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.support.HasSupportFragmentInjector;
public class ViewTagActivity extends BaseActivity implements HasSupportFragmentInjector {
@Inject
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -59,4 +69,9 @@ public class ViewTagActivity extends BaseActivity {
}
return super.onOptionsItemSelected(item);
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return dispatchingAndroidInjector;
}
}

View File

@ -27,7 +27,17 @@ import android.view.MenuItem;
import com.keylesspalace.tusky.fragment.ViewThreadFragment;
import com.keylesspalace.tusky.util.LinkHelper;
public class ViewThreadActivity extends BaseActivity {
import javax.inject.Inject;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.support.HasSupportFragmentInjector;
public class ViewThreadActivity extends BaseActivity implements HasSupportFragmentInjector {
@Inject
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -69,4 +79,9 @@ public class ViewThreadActivity extends BaseActivity {
}
return super.onOptionsItemSelected(item);
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return dispatchingAndroidInjector;
}
}

View File

@ -15,6 +15,7 @@
package com.keylesspalace.tusky.db
import android.arch.persistence.room.Database
import android.util.Log
import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.entity.Account
@ -26,12 +27,13 @@ import com.keylesspalace.tusky.entity.Account
private const val TAG = "AccountManager"
class AccountManager {
class AccountManager(db: AppDatabase) {
@Volatile var activeAccount: AccountEntity? = null
@Volatile
var activeAccount: AccountEntity? = null
private var accounts: MutableList<AccountEntity> = mutableListOf()
private val accountDao: AccountDao = TuskyApplication.getDB().accountDao()
private val accountDao: AccountDao = db.accountDao()
init {
accounts = accountDao.loadAll().toMutableList()
@ -50,9 +52,9 @@ class AccountManager {
*/
fun addAccount(accessToken: String, domain: String) {
activeAccount?.let{
activeAccount?.let {
it.isActive = false
Log.d(TAG, "addAccount: saving account with id "+it.id)
Log.d(TAG, "addAccount: saving account with id " + it.id)
accountDao.insertOrReplace(it)
}
@ -67,8 +69,8 @@ class AccountManager {
* @param account the account to save
*/
fun saveAccount(account: AccountEntity) {
if(account.id != 0L) {
Log.d(TAG, "saveAccount: saving account with id "+account.id)
if (account.id != 0L) {
Log.d(TAG, "saveAccount: saving account with id " + account.id)
accountDao.insertOrReplace(account)
}
@ -78,18 +80,18 @@ class AccountManager {
* Logs the current account out by deleting all data of the account.
* @return the new active account, or null if no other account was found
*/
fun logActiveAccountOut() : AccountEntity? {
fun logActiveAccountOut(): AccountEntity? {
if(activeAccount == null) {
if (activeAccount == null) {
return null
} else {
accounts.remove(activeAccount!!)
accountDao.delete(activeAccount!!)
if(accounts.size > 0) {
if (accounts.size > 0) {
accounts[0].isActive = true
activeAccount = accounts[0]
Log.d(TAG, "logActiveAccountOut: saving account with id "+accounts[0].id)
Log.d(TAG, "logActiveAccountOut: saving account with id " + accounts[0].id)
accountDao.insertOrReplace(accounts[0])
} else {
activeAccount = null
@ -106,18 +108,18 @@ class AccountManager {
* @param account the [Account] object returned from the api
*/
fun updateActiveAccount(account: Account) {
activeAccount?.let{
activeAccount?.let {
it.accountId = account.id
it.username = account.username
it.displayName = account.name
it.profilePictureUrl = account.avatar
Log.d(TAG, "updateActiveAccount: saving account with id "+it.id)
Log.d(TAG, "updateActiveAccount: saving account with id " + it.id)
it.id = accountDao.insertOrReplace(it)
val accountIndex = accounts.indexOf(it)
if(accountIndex != -1) {
if (accountIndex != -1) {
//in case the user was already logged in with this account, remove the old information
accounts.removeAt(accountIndex)
accounts.add(accountIndex, it)
@ -134,8 +136,8 @@ class AccountManager {
*/
fun setActiveAccount(accountId: Long) {
activeAccount?.let{
Log.d(TAG, "setActiveAccount: saving account with id "+it.id)
activeAccount?.let {
Log.d(TAG, "setActiveAccount: saving account with id " + it.id)
it.isActive = false
saveAccount(it)
}
@ -144,7 +146,7 @@ class AccountManager {
acc.id == accountId
}
activeAccount?.let{
activeAccount?.let {
it.isActive = true
accountDao.insertOrReplace(it)
}
@ -154,7 +156,7 @@ class AccountManager {
* @return an immutable list of all accounts in the database with the active account first
*/
fun getAllAccountsOrderedByActive(): List<AccountEntity> {
accounts.sortWith (Comparator { l, r ->
accounts.sortWith(Comparator { l, r ->
when {
l.isActive && !r.isActive -> -1
r.isActive && !l.isActive -> 1

View File

@ -0,0 +1,63 @@
/* Copyright 2018 charlag
*
* 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.*
import dagger.Module
import dagger.android.ContributesAndroidInjector