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 720f7c6a0c
commit a5cffe0fea
41 changed files with 1040 additions and 415 deletions

View file

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

View file

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

View file

@ -31,6 +31,7 @@ import android.support.design.widget.CollapsingToolbarLayout;
import android.support.design.widget.FloatingActionButton; import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.design.widget.TabLayout; import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager; 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.entity.Relationship;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.LinkListener; import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.pager.AccountPagerAdapter; import com.keylesspalace.tusky.pager.AccountPagerAdapter;
import com.keylesspalace.tusky.receiver.TimelineReceiver; import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.Assert; import com.keylesspalace.tusky.util.Assert;
@ -64,11 +66,17 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; 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 static final String TAG = "AccountActivity"; // logging tag
private enum FollowState { private enum FollowState {
@ -77,6 +85,11 @@ public final class AccountActivity extends BaseActivity implements ActionButtonA
REQUESTED, REQUESTED,
} }
@Inject
public MastodonApi mastodonApi;
@Inject
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
private String accountId; private String accountId;
private FollowState followState; private FollowState followState;
private boolean blocking; private boolean blocking;
@ -690,4 +703,8 @@ public final class AccountActivity extends BaseActivity implements ActionButtonA
return null; 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.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
@ -27,7 +28,16 @@ import android.view.MenuItem;
import com.keylesspalace.tusky.fragment.AccountListFragment; 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 TYPE_EXTRA = "type";
private static final String ARG_EXTRA = "arg"; private static final String ARG_EXTRA = "arg";
@ -131,4 +141,9 @@ public final class AccountListActivity extends BaseActivity {
} }
return super.onOptionsItemSelected(item); 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.preference.PreferenceManager;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.text.Spanned;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.Menu; import android.view.Menu;
import com.evernote.android.job.JobManager; import com.evernote.android.job.JobManager;
import com.evernote.android.job.JobRequest; 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.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager; 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 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 abstract class BaseActivity extends AppCompatActivity {
public MastodonApi mastodonApi;
protected Dispatcher mastodonApiDispatcher;
private AccountManager accountManager;
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
accountManager = TuskyApplication.getInstance(this).getServiceLocator()
.get(AccountManager.class);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
/* 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
@ -84,19 +64,7 @@ public abstract class BaseActivity extends AppCompatActivity {
} }
getTheme().applyStyle(style, false); getTheme().applyStyle(style, false);
if (redirectIfNotLoggedIn()) { redirectIfNotLoggedIn();
return;
}
createMastodonApi();
}
@Override
protected void onDestroy() {
if (mastodonApiDispatcher != null) {
mastodonApiDispatcher.cancelAll();
}
super.onDestroy();
} }
@Override @Override
@ -123,44 +91,13 @@ public abstract class BaseActivity extends AppCompatActivity {
return getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE); 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() { 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 intent = new Intent(this, LoginActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent); 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.AccountManager;
import com.keylesspalace.tusky.db.TootDao; import com.keylesspalace.tusky.db.TootDao;
import com.keylesspalace.tusky.db.TootEntity; import com.keylesspalace.tusky.db.TootEntity;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.fragment.ComposeOptionsFragment; import com.keylesspalace.tusky.fragment.ComposeOptionsFragment;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.ProgressRequestBody; import com.keylesspalace.tusky.network.ProgressRequestBody;
import com.keylesspalace.tusky.util.CountUpDownLatch; import com.keylesspalace.tusky.util.CountUpDownLatch;
import com.keylesspalace.tusky.util.DownsizeImageTask; import com.keylesspalace.tusky.util.DownsizeImageTask;
@ -115,6 +117,8 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import retrofit2.Call; import retrofit2.Call;
@ -122,7 +126,9 @@ import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
public final class ComposeActivity extends BaseActivity 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 String TAG = "ComposeActivity"; // logging tag
private static final int STATUS_CHARACTER_LIMIT = 500; private static final int STATUS_CHARACTER_LIMIT = 500;
private static final int STATUS_MEDIA_SIZE_LIMIT = 8388608; // 8MiB 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(); private static TootDao tootDao = TuskyApplication.getDB().tootDao();
@Inject
public MastodonApi mastodonApi;
@Inject
public AccountManager accountManager;
private TextView replyTextView; private TextView replyTextView;
private TextView replyContentTextView; private TextView replyContentTextView;
private EditTextTyped textEditor; private EditTextTyped textEditor;
@ -206,11 +217,9 @@ public final class ComposeActivity extends BaseActivity
} }
// setup the account image // setup the account image
AccountEntity activeAccount = TuskyApplication.getInstance(this).getServiceLocator() final AccountEntity activeAccount = accountManager.getActiveAccount();
.get(AccountManager.class).getActiveAccount();
if (activeAccount != null) { if (activeAccount != null) {
ImageView composeAvatar = findViewById(R.id.composeAvatar); ImageView composeAvatar = findViewById(R.id.composeAvatar);
if (TextUtils.isEmpty(activeAccount.getProfilePictureUrl())) { if (TextUtils.isEmpty(activeAccount.getProfilePictureUrl())) {

View file

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

View file

@ -25,7 +25,17 @@ import android.view.MenuItem;
import com.keylesspalace.tusky.fragment.TimelineFragment; 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 @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -56,4 +66,9 @@ public class FavouritesActivity extends BaseActivity {
} }
return super.onOptionsItemSelected(item); 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.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
import com.varunest.sparkbutton.helpers.Utils
import retrofit2.Call import retrofit2.Call
import retrofit2.Response import retrofit2.Response
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import javax.inject.Inject
/** /**
* Created by charlag on 1/4/18. * 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 { companion object {
@JvmStatic @JvmStatic
@ -92,6 +92,9 @@ class ListsActivity : BaseActivity(), ListsView {
} }
} }
@Inject
lateinit var mastodonApi: MastodonApi
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
private lateinit var progressBar: ProgressBar 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.FloatingActionButton;
import android.support.design.widget.TabLayout; import android.support.design.widget.TabLayout;
import android.support.graphics.drawable.VectorDrawableCompat; import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewPager; 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.db.AccountManager;
import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.pager.TimelinePagerAdapter; import com.keylesspalace.tusky.pager.TimelinePagerAdapter;
import com.keylesspalace.tusky.receiver.TimelineReceiver; import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.NotificationHelper; import com.keylesspalace.tusky.util.NotificationHelper;
@ -63,11 +65,18 @@ import com.squareup.picasso.Picasso;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; 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 String TAG = "MainActivity"; // logging tag
private static final long DRAWER_ITEM_ADD_ACCOUNT = -13; private static final long DRAWER_ITEM_ADD_ACCOUNT = -13;
private static final long DRAWER_ITEM_EDIT_PROFILE = 0; 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_SAVED_TOOT = 9;
private static final long DRAWER_ITEM_LISTS = 10; private static final long DRAWER_ITEM_LISTS = 10;
@Inject
public MastodonApi mastodonApi;
@Inject
public DispatchingAndroidInjector<Fragment> fragmentInjector;
private static int COMPOSE_RESULT = 1; private static int COMPOSE_RESULT = 1;
AccountManager accountManager; AccountManager accountManager;
@ -93,10 +107,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
// account switching has to be done before MastodonApi is created in super.onCreate
Intent intent = getIntent(); Intent intent = getIntent();
int tabPosition = 0; int tabPosition = 0;
accountManager = TuskyApplication.getInstance(this).getServiceLocator() accountManager = TuskyApplication.getInstance(this).getServiceLocator()
@ -550,4 +561,9 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
public FloatingActionButton getActionButton() { public FloatingActionButton getActionButton() {
return composeButton; 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.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.FloatingActionButton import android.support.design.widget.FloatingActionButton
import android.support.v4.app.Fragment
import android.support.v7.widget.Toolbar import android.support.v7.widget.Toolbar
import android.view.MenuItem import android.view.MenuItem
import android.widget.FrameLayout import android.widget.FrameLayout
import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.interfaces.ActionButtonActivity 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 { companion object {
private const val ARG_KIND = "kind" private const val ARG_KIND = "kind"
private const val ARG_ARG = "arg" 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) val intent = Intent(context, ModalTimelineActivity::class.java)
intent.putExtra(ARG_KIND, kind) intent.putExtra(ARG_KIND, kind)
intent.putExtra(ARG_ARG, argument) intent.putExtra(ARG_ARG, argument)
@ -24,6 +34,7 @@ class ModalTimelineActivity : BaseActivity(), ActionButtonActivity {
} }
} }
lateinit var contentFrame: FrameLayout lateinit var contentFrame: FrameLayout
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -41,8 +52,8 @@ class ModalTimelineActivity : BaseActivity(), ActionButtonActivity {
} }
if (supportFragmentManager.findFragmentById(R.id.content_frame) == null) { if (supportFragmentManager.findFragmentById(R.id.content_frame) == null) {
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineFragment.Kind ?: val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineFragment.Kind
TimelineFragment.Kind.HOME ?: TimelineFragment.Kind.HOME
val argument = intent?.getStringExtra(ARG_ARG) val argument = intent?.getStringExtra(ARG_ARG)
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.content_frame, TimelineFragment.newInstance(kind, argument)) .replace(R.id.content_frame, TimelineFragment.newInstance(kind, argument))
@ -60,4 +71,8 @@ class ModalTimelineActivity : BaseActivity(), ActionButtonActivity {
} }
return false return false
} }
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return dispatchingAndroidInjector
}
} }

View file

@ -16,23 +16,17 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.Spanned;
import android.util.Log; import android.util.Log;
import com.evernote.android.job.Job; import com.evernote.android.job.Job;
import com.evernote.android.job.JobCreator; 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.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.json.SpannedTypeAdapter;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.NotificationHelper; import com.keylesspalace.tusky.util.NotificationHelper;
import com.keylesspalace.tusky.util.OkHttpUtils;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
@ -40,10 +34,9 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import okhttp3.OkHttpClient; import javax.inject.Inject;
import retrofit2.Response; import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
/** /**
* Created by charlag on 31/10/17. * 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"; 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.context = context;
this.accountManager = accountManager;
} }
@Nullable @Nullable
@Override @Override
public Job create(@NonNull String tag) { public Job create(@NonNull String tag) {
if (tag.equals(NOTIFICATIONS_JOB_TAG)) { if (tag.equals(NOTIFICATIONS_JOB_TAG)) {
return new NotificationPullJob(context); return new NotificationPullJob(context, accountManager, api);
} }
return null; 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 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.context = context;
this.accountManager = accountManager;
this.mastodonApi = mastodonApi;
} }
@NonNull @NonNull
@Override @Override
protected Result onRunJob(@NonNull Params params) { protected Result onRunJob(@NonNull Params params) {
AccountManager accountManager = TuskyApplication.getInstance(context).getServiceLocator()
.get(AccountManager.class);
List<AccountEntity> accountList = new ArrayList<>(accountManager.getAllAccountsOrderedByActive()); List<AccountEntity> accountList = new ArrayList<>(accountManager.getAllAccountsOrderedByActive());
for (AccountEntity account : accountList) { for (AccountEntity account : accountList) {
if (account.getNotificationsEnabled()) { if (account.getNotificationsEnabled()) {
MastodonApi api = createMastodonApi(account.getDomain(), context);
try { try {
Log.d(TAG, "getting Notifications for " + account.getFullName()); Log.d(TAG, "getting Notifications for " + account.getFullName());
Response<List<Notification>> notifications = 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()) { if (notifications.isSuccessful()) {
onNotificationsReceived(account, notifications.body()); onNotificationsReceived(account, notifications.body());
} else { } else {
@ -127,22 +108,15 @@ public final class NotificationPullJobCreator implements JobCreator {
} }
return Result.SUCCESS; return Result.SUCCESS;
} }
private void onNotificationsReceived(AccountEntity account, List<Notification> notificationList) { private void onNotificationsReceived(AccountEntity account, List<Notification> notificationList) {
Collections.reverse(notificationList); Collections.reverse(notificationList);
BigInteger newId = new BigInteger(account.getLastNotificationId()); BigInteger newId = new BigInteger(account.getLastNotificationId());
BigInteger newestId = BigInteger.ZERO; BigInteger newestId = BigInteger.ZERO;
for (Notification notification : notificationList) { for (Notification notification : notificationList) {
BigInteger currentId = new BigInteger(notification.getId()); BigInteger currentId = new BigInteger(notification.getId());
if (isBiggerThan(currentId, newestId)) { if (isBiggerThan(currentId, newestId)) {
newestId = currentId; newestId = currentId;
} }
@ -153,12 +127,10 @@ public final class NotificationPullJobCreator implements JobCreator {
} }
account.setLastNotificationId(newestId.toString()); account.setLastNotificationId(newestId.toString());
TuskyApplication.getInstance(context).getServiceLocator() accountManager.saveAccount(account);
.get(AccountManager.class).saveAccount(account);
} }
private boolean isBiggerThan(BigInteger newId, BigInteger lastShownNotificationId) { private boolean isBiggerThan(BigInteger newId, BigInteger lastShownNotificationId) {
return lastShownNotificationId.compareTo(newId) == -1; return lastShownNotificationId.compareTo(newId) == -1;
} }
} }

View file

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

View file

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

View file

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

View file

@ -25,7 +25,17 @@ import android.view.MenuItem;
import com.keylesspalace.tusky.fragment.TimelineFragment; 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 @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -59,4 +69,9 @@ public class ViewTagActivity extends BaseActivity {
} }
return super.onOptionsItemSelected(item); 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.fragment.ViewThreadFragment;
import com.keylesspalace.tusky.util.LinkHelper; 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 @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -69,4 +79,9 @@ public class ViewThreadActivity extends BaseActivity {
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return dispatchingAndroidInjector;
}
} }

View file

@ -15,6 +15,7 @@
package com.keylesspalace.tusky.db package com.keylesspalace.tusky.db
import android.arch.persistence.room.Database
import android.util.Log import android.util.Log
import com.keylesspalace.tusky.TuskyApplication import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
@ -26,12 +27,13 @@ import com.keylesspalace.tusky.entity.Account
private const val TAG = "AccountManager" 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 var accounts: MutableList<AccountEntity> = mutableListOf()
private val accountDao: AccountDao = TuskyApplication.getDB().accountDao() private val accountDao: AccountDao = db.accountDao()
init { init {
accounts = accountDao.loadAll().toMutableList() accounts = accountDao.loadAll().toMutableList()
@ -50,9 +52,9 @@ class AccountManager {
*/ */
fun addAccount(accessToken: String, domain: String) { fun addAccount(accessToken: String, domain: String) {
activeAccount?.let{ activeAccount?.let {
it.isActive = false 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) accountDao.insertOrReplace(it)
} }
@ -67,8 +69,8 @@ class AccountManager {
* @param account the account to save * @param account the account to save
*/ */
fun saveAccount(account: AccountEntity) { fun saveAccount(account: AccountEntity) {
if(account.id != 0L) { if (account.id != 0L) {
Log.d(TAG, "saveAccount: saving account with id "+account.id) Log.d(TAG, "saveAccount: saving account with id " + account.id)
accountDao.insertOrReplace(account) accountDao.insertOrReplace(account)
} }
@ -78,18 +80,18 @@ class AccountManager {
* Logs the current account out by deleting all data of the account. * 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 * @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 return null
} else { } else {
accounts.remove(activeAccount!!) accounts.remove(activeAccount!!)
accountDao.delete(activeAccount!!) accountDao.delete(activeAccount!!)
if(accounts.size > 0) { if (accounts.size > 0) {
accounts[0].isActive = true accounts[0].isActive = true
activeAccount = accounts[0] 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]) accountDao.insertOrReplace(accounts[0])
} else { } else {
activeAccount = null activeAccount = null
@ -106,18 +108,18 @@ class AccountManager {
* @param account the [Account] object returned from the api * @param account the [Account] object returned from the api
*/ */
fun updateActiveAccount(account: Account) { fun updateActiveAccount(account: Account) {
activeAccount?.let{ activeAccount?.let {
it.accountId = account.id it.accountId = account.id
it.username = account.username it.username = account.username
it.displayName = account.name it.displayName = account.name
it.profilePictureUrl = account.avatar 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) it.id = accountDao.insertOrReplace(it)
val accountIndex = accounts.indexOf(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 //in case the user was already logged in with this account, remove the old information
accounts.removeAt(accountIndex) accounts.removeAt(accountIndex)
accounts.add(accountIndex, it) accounts.add(accountIndex, it)
@ -134,8 +136,8 @@ class AccountManager {
*/ */
fun setActiveAccount(accountId: Long) { fun setActiveAccount(accountId: Long) {
activeAccount?.let{ activeAccount?.let {
Log.d(TAG, "setActiveAccount: saving account with id "+it.id) Log.d(TAG, "setActiveAccount: saving account with id " + it.id)
it.isActive = false it.isActive = false
saveAccount(it) saveAccount(it)
} }
@ -144,7 +146,7 @@ class AccountManager {
acc.id == accountId acc.id == accountId
} }
activeAccount?.let{ activeAccount?.let {
it.isActive = true it.isActive = true
accountDao.insertOrReplace(it) accountDao.insertOrReplace(it)
} }
@ -154,7 +156,7 @@ class AccountManager {
* @return an immutable list of all accounts in the database with the active account first * @return an immutable list of all accounts in the database with the active account first
*/ */
fun getAllAccountsOrderedByActive(): List<AccountEntity> { fun getAllAccountsOrderedByActive(): List<AccountEntity> {
accounts.sortWith (Comparator { l, r -> accounts.sortWith(Comparator { l, r ->
when { when {
l.isActive && !r.isActive -> -1 l.isActive && !r.isActive -> -1
r.isActive && !l.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
/**
* Created by charlag on 3/24/18.
*/
@Module
abstract class ActivitiesModule {
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesMainActivity(): MainActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesAccountActivity(): AccountActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesListsActivity(): ListsActivity
@ContributesAndroidInjector
abstract fun contributesComposeActivity(): ComposeActivity
@ContributesAndroidInjector
abstract fun contributesEditProfileActivity(): EditProfileActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesAccountListActivity(): AccountListActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesModalTimelineActivity(): ModalTimelineActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesViewTagActivity(): ViewTagActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesViewThreadActivity(): ViewThreadActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesFavouritesActivity(): FavouritesActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contribtutesSearchAvtivity(): SearchActivity
@ContributesAndroidInjector()
abstract fun contributesAboutActivity(): AboutActivity
}

View file

@ -0,0 +1,46 @@
/* 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.TuskyApplication
import dagger.BindsInstance
import dagger.Component
import dagger.android.AndroidInjectionModule
import javax.inject.Singleton
/**
* Created by charlag on 3/21/18.
*/
@Singleton
@Component(modules = [
AppModule::class,
NetworkModule::class,
AndroidInjectionModule::class,
ActivitiesModule::class
])
interface AppComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun application(tuskyApp: TuskyApplication): Builder
fun build(): AppComponent
}
fun inject(app: TuskyApplication)
}

View file

@ -0,0 +1,80 @@
/* 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 android.app.Activity
import android.app.Application
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentActivity
import android.support.v4.app.FragmentManager
import com.keylesspalace.tusky.TuskyApplication
import dagger.android.AndroidInjection
import dagger.android.support.AndroidSupportInjection
import dagger.android.support.HasSupportFragmentInjector
/**
* Created by charlag on 3/24/18.
*/
object AppInjector {
fun init(app: TuskyApplication) {
DaggerAppComponent.builder().application(app)
.build().inject(app)
app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
handleActivity(activity)
}
override fun onActivityPaused(activity: Activity?) {
}
override fun onActivityResumed(activity: Activity?) {
}
override fun onActivityStarted(activity: Activity?) {
}
override fun onActivityDestroyed(activity: Activity?) {
}
override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {
}
override fun onActivityStopped(activity: Activity?) {
}
})
}
private fun handleActivity(activity: Activity) {
if (activity is HasSupportFragmentInjector || activity is Injectable) {
AndroidInjection.inject(activity)
}
if (activity is FragmentActivity) {
activity.supportFragmentManager.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentCreated(fm: FragmentManager?, f: Fragment?,
savedInstanceState: Bundle?) {
if (f is Injectable) {
AndroidSupportInjection.inject(f)
}
}
}, true)
}
}
}

View file

@ -0,0 +1,68 @@
/* 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 android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
import android.support.v4.content.LocalBroadcastManager
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.network.TimelineCasesImpl
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
/**
* Created by charlag on 3/21/18.
*/
@Module
class AppModule {
@Provides
fun providesApplication(app: TuskyApplication): Application = app
@Provides
fun providesContext(app: Application): Context = app
@Provides
fun providesSharedPreferences(app: Application): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(app)
}
@Provides
fun providesBroadcastManager(app: Application): LocalBroadcastManager {
return LocalBroadcastManager.getInstance(app)
}
@Provides
fun providesTimelineUseCases(api: MastodonApi,
broadcastManager: LocalBroadcastManager): TimelineCases {
return TimelineCasesImpl(api, broadcastManager)
}
@Provides
@Singleton
fun providesAccountManager(app: TuskyApplication): AccountManager {
return app.serviceLocator.get(AccountManager::class.java)
}
}

View file

@ -0,0 +1,43 @@
/* 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.fragment.*
import dagger.Module
import dagger.android.ContributesAndroidInjector
/**
* Created by charlag on 3/24/18.
*/
@Module
abstract class FragmentBuildersModule {
@ContributesAndroidInjector
abstract fun accountListFragment(): AccountListFragment
@ContributesAndroidInjector
abstract fun accountMediaFragment(): AccountMediaFragment
@ContributesAndroidInjector
abstract fun viewThreadFragment(): ViewThreadFragment
@ContributesAndroidInjector
abstract fun timelineFragment(): TimelineFragment
@ContributesAndroidInjector
abstract fun notificationsFragment(): NotificationsFragment
}

View file

@ -0,0 +1,23 @@
/* 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
/**
* Created by charlag on 3/24/18.
*/
interface Injectable

View file

@ -0,0 +1,111 @@
/* 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 android.content.SharedPreferences
import android.text.Spanned
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.json.SpannedTypeAdapter
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.OkHttpUtils
import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import dagger.multibindings.IntoSet
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
/**
* Created by charlag on 3/24/18.
*/
@Module
class NetworkModule {
@Provides
@IntoMap()
@ClassKey(Spanned::class)
fun providesSpannedTypeAdapter(): JsonDeserializer<*> = SpannedTypeAdapter()
@Provides
@Singleton
fun providesGson(adapters: @JvmSuppressWildcards Map<Class<*>, JsonDeserializer<*>>): Gson {
return GsonBuilder()
.apply {
for ((k, v) in adapters) {
registerTypeAdapter(k, v)
}
}
.create()
}
@Provides
@IntoSet
@Singleton
fun providesConverterFactory(gson: Gson): Converter.Factory = GsonConverterFactory.create(gson)
@Provides
@IntoSet
@Singleton
fun providesAuthInterceptor(accountManager: AccountManager): Interceptor {
// should accept AccountManager here probably but I don't want to break things yet
return InstanceSwitchAuthInterceptor(accountManager)
}
@Provides
@Singleton
fun providesHttpClient(interceptors: @JvmSuppressWildcards Set<Interceptor>,
preferences: SharedPreferences): OkHttpClient {
return OkHttpUtils.getCompatibleClientBuilder(preferences)
.apply {
interceptors.fold(this) { b, i ->
b.addInterceptor(i)
}
}
.build()
}
@Provides
@Singleton
fun providesRetrofit(httpClient: OkHttpClient,
converters: @JvmSuppressWildcards Set<Converter.Factory>): Retrofit {
return Retrofit.Builder().baseUrl("https://dummy.placeholder/")
.client(httpClient)
.let { builder ->
// Doing it this way in case builder will be immutable so we return the final
// instance
converters.fold(builder) { b, c ->
b.addConverterFactory(c)
}
}
.build()
}
@Provides
@Singleton
fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create(MastodonApi::class.java)
}

View file

@ -39,6 +39,7 @@ import com.keylesspalace.tusky.adapter.FollowAdapter;
import com.keylesspalace.tusky.adapter.FollowRequestsAdapter; import com.keylesspalace.tusky.adapter.FollowRequestsAdapter;
import com.keylesspalace.tusky.adapter.FooterViewHolder; import com.keylesspalace.tusky.adapter.FooterViewHolder;
import com.keylesspalace.tusky.adapter.MutesAdapter; import com.keylesspalace.tusky.adapter.MutesAdapter;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Relationship; import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.keylesspalace.tusky.interfaces.AccountActionListener;
@ -49,11 +50,14 @@ import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
public class AccountListFragment extends BaseFragment implements AccountActionListener { public class AccountListFragment extends BaseFragment implements AccountActionListener,
Injectable {
private static final String TAG = "AccountList"; // logging tag private static final String TAG = "AccountList"; // logging tag
public AccountListFragment() { public AccountListFragment() {
@ -67,13 +71,15 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
FOLLOW_REQUESTS, FOLLOW_REQUESTS,
} }
@Inject
public MastodonApi api;
private Type type; private Type type;
private String accountId; private String accountId;
private LinearLayoutManager layoutManager; private LinearLayoutManager layoutManager;
private RecyclerView recyclerView; private RecyclerView recyclerView;
private EndlessOnScrollListener scrollListener; private EndlessOnScrollListener scrollListener;
private AccountAdapter adapter; private AccountAdapter adapter;
private MastodonApi api;
private boolean bottomLoading; private boolean bottomLoading;
private int bottomFetches; private int bottomFetches;
private boolean topLoading; private boolean topLoading;
@ -146,12 +152,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
@Override @Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) { public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
BaseActivity activity = (BaseActivity) getActivity();
/* MastodonApi on the base activity is only guaranteed to be initialised after the parent
* activity is created, so everything needing to access the api object has to be delayed
* until here. */
api = activity.mastodonApi;
// Just use the basic scroll listener to load more accounts. // Just use the basic scroll listener to load more accounts.
scrollListener = new EndlessOnScrollListener(layoutManager) { scrollListener = new EndlessOnScrollListener(layoutManager) {
@Override @Override
@ -464,7 +464,7 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
private void onLoadMore(RecyclerView recyclerView) { private void onLoadMore(RecyclerView recyclerView) {
AccountAdapter adapter = (AccountAdapter) recyclerView.getAdapter(); AccountAdapter adapter = (AccountAdapter) recyclerView.getAdapter();
//if we do not have a bottom id, we know we do not need to load more //if we do not have a bottom id, we know we do not need to load more
if(adapter.getBottomId() == null) return; if (adapter.getBottomId() == null) return;
fetchAccounts(adapter.getBottomId(), null, FetchEnd.BOTTOM); fetchAccounts(adapter.getBottomId(), null, FetchEnd.BOTTOM);
} }

View file

@ -29,10 +29,10 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.ViewVideoActivity import com.keylesspalace.tusky.ViewVideoActivity
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
@ -43,6 +43,7 @@ import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import java.util.* import java.util.*
import javax.inject.Inject
/** /**
* Created by charlag on 26/10/2017. * Created by charlag on 26/10/2017.
@ -50,7 +51,7 @@ import java.util.*
* Fragment with multiple columns of media previews for the specified account. * Fragment with multiple columns of media previews for the specified account.
*/ */
class AccountMediaFragment : BaseFragment() { class AccountMediaFragment : BaseFragment(), Injectable {
companion object { companion object {
@JvmStatic @JvmStatic
@ -66,9 +67,11 @@ class AccountMediaFragment : BaseFragment() {
private const val TAG = "AccountMediaFragment" private const val TAG = "AccountMediaFragment"
} }
@Inject
lateinit var api: MastodonApi
private val adapter = MediaGridAdapter() private val adapter = MediaGridAdapter()
private var currentCall: Call<List<Status>>? = null private var currentCall: Call<List<Status>>? = null
private lateinit var api: MastodonApi
private val statuses = mutableListOf<Status>() private val statuses = mutableListOf<Status>()
private var fetchingStatus = FetchingStatus.NOT_FETCHING private var fetchingStatus = FetchingStatus.NOT_FETCHING
lateinit private var swipeLayout: SwipeRefreshLayout lateinit private var swipeLayout: SwipeRefreshLayout
@ -123,7 +126,6 @@ class AccountMediaFragment : BaseFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
api = (activity as BaseActivity).mastodonApi
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,

View file

@ -15,14 +15,10 @@
package com.keylesspalace.tusky.fragment; package com.keylesspalace.tusky.fragment;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import com.keylesspalace.tusky.R;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -44,9 +40,4 @@ public class BaseFragment extends Fragment {
} }
super.onDestroy(); super.onDestroy();
} }
protected SharedPreferences getPrivatePreferences() {
return getContext().getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
}
} }

View file

@ -43,11 +43,14 @@ 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;
import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases;
import com.keylesspalace.tusky.receiver.TimelineReceiver; import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.CollectionUtil; import com.keylesspalace.tusky.util.CollectionUtil;
import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.Either;
@ -64,14 +67,18 @@ import java.math.BigInteger;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
public class NotificationsFragment extends SFragment implements public class NotificationsFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
NotificationsAdapter.NotificationActionListener, NotificationsAdapter.NotificationActionListener,
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener,
Injectable {
private static final String TAG = "NotificationF"; // logging tag private static final String TAG = "NotificationF"; // logging tag
private static final int LOAD_AT_ONCE = 30; private static final int LOAD_AT_ONCE = 30;
@ -97,6 +104,11 @@ public class NotificationsFragment extends SFragment implements
} }
} }
@Inject
public TimelineCases timelineCases;
@Inject
public MastodonApi mastodonApi;
private SwipeRefreshLayout swipeRefreshLayout; private SwipeRefreshLayout swipeRefreshLayout;
private LinearLayoutManager layoutManager; private LinearLayoutManager layoutManager;
private RecyclerView recyclerView; private RecyclerView recyclerView;
@ -113,6 +125,11 @@ public class NotificationsFragment extends SFragment implements
private String topId; private String topId;
private boolean alwaysShowSensitiveMedia; private boolean alwaysShowSensitiveMedia;
@Override
protected TimelineCases timelineCases() {
return timelineCases;
}
// Each element is either a Notification for loading data or a Placeholder // Each element is either a Notification for loading data or a Placeholder
private final PairedList<Either<Placeholder, Notification>, NotificationViewData> notifications private final PairedList<Either<Placeholder, Notification>, NotificationViewData> notifications
= new PairedList<>(new Function<Either<Placeholder, Notification>, NotificationViewData>() { = new PairedList<>(new Function<Either<Placeholder, Notification>, NotificationViewData>() {
@ -273,7 +290,7 @@ public class NotificationsFragment extends SFragment implements
public void onReblog(final boolean reblog, final int position) { public void onReblog(final boolean reblog, final int position) {
final Notification notification = notifications.get(position).getAsRight(); final Notification notification = notifications.get(position).getAsRight();
final Status status = notification.getStatus(); final Status status = notification.getStatus();
reblogWithCallback(status, reblog, new Callback<Status>() { timelineCases.reblogWithCallback(status, reblog, new Callback<Status>() {
@Override @Override
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) { public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
@ -310,7 +327,7 @@ public class NotificationsFragment extends SFragment implements
public void onFavourite(final boolean favourite, final int position) { public void onFavourite(final boolean favourite, final int position) {
final Notification notification = notifications.get(position).getAsRight(); final Notification notification = notifications.get(position).getAsRight();
final Status status = notification.getStatus(); final Status status = notification.getStatus();
favouriteWithCallback(status, favourite, new Callback<Status>() { timelineCases.favouriteWithCallback(status, favourite, new Callback<Status>() {
@Override @Override
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) { public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
if (response.isSuccessful()) { if (response.isSuccessful()) {

View file

@ -41,18 +41,22 @@ import com.keylesspalace.tusky.ViewTagActivity;
import com.keylesspalace.tusky.ViewThreadActivity; import com.keylesspalace.tusky.ViewThreadActivity;
import com.keylesspalace.tusky.ViewVideoActivity; import com.keylesspalace.tusky.ViewVideoActivity;
import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Relationship; import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.AdapterItemRemover; import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases;
import com.keylesspalace.tusky.receiver.TimelineReceiver; import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.HtmlUtils; import com.keylesspalace.tusky.util.HtmlUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
@ -69,7 +73,8 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
protected String loggedInAccountId; protected String loggedInAccountId;
protected String loggedInUsername; protected String loggedInUsername;
protected MastodonApi mastodonApi;
protected abstract TimelineCases timelineCases();
@Override @Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) { public void onActivityCreated(@Nullable Bundle savedInstanceState) {
@ -80,8 +85,6 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
loggedInAccountId = activeAccount.getAccountId(); loggedInAccountId = activeAccount.getAccountId();
loggedInUsername = activeAccount.getUsername(); loggedInUsername = activeAccount.getUsername();
} }
BaseActivity activity = (BaseActivity) getActivity();
mastodonApi = activity.mastodonApi;
} }
@Override @Override
@ -90,6 +93,11 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left); getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left);
} }
protected void openReblog(@Nullable final Status status) {
if (status == null) return;
viewAccount(status.getAccount().getId());
}
protected void reply(Status status) { protected void reply(Status status) {
String inReplyToId = status.getActionableId(); String inReplyToId = status.getActionableId();
Status actionableStatus = status.getActionableStatus(); Status actionableStatus = status.getActionableStatus();
@ -113,88 +121,6 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
startActivityForResult(intent, COMPOSE_RESULT); startActivityForResult(intent, COMPOSE_RESULT);
} }
protected void reblogWithCallback(final Status status, final boolean reblog,
Callback<Status> callback) {
String id = status.getActionableId();
Call<Status> call;
if (reblog) {
call = mastodonApi.reblogStatus(id);
} else {
call = mastodonApi.unreblogStatus(id);
}
call.enqueue(callback);
}
protected void favouriteWithCallback(final Status status, final boolean favourite,
final Callback<Status> callback) {
String id = status.getActionableId();
Call<Status> call;
if (favourite) {
call = mastodonApi.favouriteStatus(id);
} else {
call = mastodonApi.unfavouriteStatus(id);
}
call.enqueue(callback);
callList.add(call);
}
protected void openReblog(@Nullable final Status status) {
if (status == null) return;
viewAccount(status.getAccount().getId());
}
private void mute(String id) {
Call<Relationship> call = mastodonApi.muteAccount(id);
call.enqueue(new Callback<Relationship>() {
@Override
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {
}
@Override
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {
}
});
callList.add(call);
Intent intent = new Intent(TimelineReceiver.Types.MUTE_ACCOUNT);
intent.putExtra("id", id);
LocalBroadcastManager.getInstance(getContext())
.sendBroadcast(intent);
}
private void block(String id) {
Call<Relationship> call = mastodonApi.blockAccount(id);
call.enqueue(new Callback<Relationship>() {
@Override
public void onResponse(@NonNull Call<Relationship> call, @NonNull retrofit2.Response<Relationship> response) {
}
@Override
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {
}
});
callList.add(call);
Intent intent = new Intent(TimelineReceiver.Types.BLOCK_ACCOUNT);
intent.putExtra("id", id);
LocalBroadcastManager.getInstance(getContext())
.sendBroadcast(intent);
}
private void delete(String id) {
Call<ResponseBody> call = mastodonApi.deleteStatus(id);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(@NonNull Call<ResponseBody> call, @NonNull retrofit2.Response<ResponseBody> response) {
}
@Override
public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {
}
});
callList.add(call);
}
protected void more(final Status status, View view, final int position) { protected void more(final Status status, View view, final int position) {
final String id = status.getActionableId(); final String id = status.getActionableId();
final String accountId = status.getActionableStatus().getAccount().getId(); final String accountId = status.getActionableStatus().getAccount().getId();
@ -208,60 +134,56 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
} else { } else {
popup.inflate(R.menu.status_more_for_user); popup.inflate(R.menu.status_more_for_user);
} }
popup.setOnMenuItemClickListener( popup.setOnMenuItemClickListener(item -> {
new PopupMenu.OnMenuItemClickListener() { switch (item.getItemId()) {
@Override case R.id.status_share_content: {
public boolean onMenuItemClick(MenuItem item) { StringBuilder sb = new StringBuilder();
switch (item.getItemId()) { sb.append(status.getAccount().getUsername());
case R.id.status_share_content: { sb.append(" - ");
StringBuilder sb = new StringBuilder(); sb.append(status.getContent().toString());
sb.append(status.getAccount().getUsername());
sb.append(" - ");
sb.append(status.getContent().toString());
Intent sendIntent = new Intent(); Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND); sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, sb.toString()); sendIntent.putExtra(Intent.EXTRA_TEXT, sb.toString());
sendIntent.setType("text/plain"); sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to))); startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to)));
return true; return true;
} }
case R.id.status_share_link: { case R.id.status_share_link: {
Intent sendIntent = new Intent(); Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND); sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl); sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl);
sendIntent.setType("text/plain"); sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_link_to))); startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_link_to)));
return true; return true;
} }
case R.id.status_copy_link: { case R.id.status_copy_link: {
ClipboardManager clipboard = (ClipboardManager) ClipboardManager clipboard = (ClipboardManager)
getActivity().getSystemService(Context.CLIPBOARD_SERVICE); getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(null, statusUrl); ClipData clip = ClipData.newPlainText(null, statusUrl);
clipboard.setPrimaryClip(clip); clipboard.setPrimaryClip(clip);
return true; return true;
} }
case R.id.status_mute: { case R.id.status_mute: {
mute(accountId); timelineCases().mute(accountId);
return true; return true;
} }
case R.id.status_block: { case R.id.status_block: {
block(accountId); timelineCases().block(accountId);
return true; return true;
} }
case R.id.status_report: { case R.id.status_report: {
openReportPage(accountId, accountUsename, id, content); openReportPage(accountId, accountUsename, id, content);
return true; return true;
} }
case R.id.status_delete: { case R.id.status_delete: {
delete(id); timelineCases().delete(id);
removeItem(position); removeItem(position);
return true; return true;
} }
} }
return false; return false;
} });
});
popup.show(); popup.show();
} }

View file

@ -40,11 +40,13 @@ import com.keylesspalace.tusky.BuildConfig;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.adapter.FooterViewHolder; import com.keylesspalace.tusky.adapter.FooterViewHolder;
import com.keylesspalace.tusky.adapter.TimelineAdapter; import com.keylesspalace.tusky.adapter.TimelineAdapter;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases;
import com.keylesspalace.tusky.receiver.TimelineReceiver; import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.CollectionUtil; import com.keylesspalace.tusky.util.CollectionUtil;
import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.Either;
@ -60,6 +62,8 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
@ -67,7 +71,8 @@ import retrofit2.Response;
public class TimelineFragment extends SFragment implements public class TimelineFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, SwipeRefreshLayout.OnRefreshListener,
StatusActionListener, StatusActionListener,
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener,
Injectable {
private static final String TAG = "TimelineF"; // logging tag private static final String TAG = "TimelineF"; // logging tag
private static final String KIND_ARG = "kind"; private static final String KIND_ARG = "kind";
private static final String HASHTAG_OR_ID_ARG = "hashtag_or_id"; private static final String HASHTAG_OR_ID_ARG = "hashtag_or_id";
@ -90,6 +95,11 @@ public class TimelineFragment extends SFragment implements
MIDDLE MIDDLE
} }
@Inject
TimelineCases timelineCases;
@Inject
MastodonApi mastodonApi;
private SwipeRefreshLayout swipeRefreshLayout; private SwipeRefreshLayout swipeRefreshLayout;
private TimelineAdapter adapter; private TimelineAdapter adapter;
private Kind kind; private Kind kind;
@ -113,6 +123,11 @@ public class TimelineFragment extends SFragment implements
private boolean alwaysShowSensitiveMedia; private boolean alwaysShowSensitiveMedia;
@Override
protected TimelineCases timelineCases() {
return timelineCases;
}
private PairedList<Either<Placeholder, Status>, StatusViewData> statuses = private PairedList<Either<Placeholder, Status>, StatusViewData> statuses =
new PairedList<>(new Function<Either<Placeholder, Status>, StatusViewData>() { new PairedList<>(new Function<Either<Placeholder, Status>, StatusViewData>() {
@Override @Override
@ -307,7 +322,7 @@ public class TimelineFragment extends SFragment implements
@Override @Override
public void onReblog(final boolean reblog, final int position) { public void onReblog(final boolean reblog, final int position) {
final Status status = statuses.get(position).getAsRight(); final Status status = statuses.get(position).getAsRight();
super.reblogWithCallback(status, reblog, new Callback<Status>() { timelineCases.reblogWithCallback(status, reblog, new Callback<Status>() {
@Override @Override
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) { public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
@ -342,7 +357,7 @@ public class TimelineFragment extends SFragment implements
public void onFavourite(final boolean favourite, final int position) { public void onFavourite(final boolean favourite, final int position) {
final Status status = statuses.get(position).getAsRight(); final Status status = statuses.get(position).getAsRight();
super.favouriteWithCallback(status, favourite, new Callback<Status>() { timelineCases.favouriteWithCallback(status, favourite, new Callback<Status>() {
@Override @Override
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) { public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {

View file

@ -37,11 +37,14 @@ import android.view.ViewGroup;
import com.keylesspalace.tusky.BuildConfig; import com.keylesspalace.tusky.BuildConfig;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.adapter.ThreadAdapter; import com.keylesspalace.tusky.adapter.ThreadAdapter;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StatusContext; import com.keylesspalace.tusky.entity.StatusContext;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases;
import com.keylesspalace.tusky.receiver.TimelineReceiver; import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
@ -53,14 +56,21 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
public class ViewThreadFragment extends SFragment implements public class ViewThreadFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener { SwipeRefreshLayout.OnRefreshListener, StatusActionListener, Injectable {
private static final String TAG = "ViewThreadFragment"; private static final String TAG = "ViewThreadFragment";
@Inject
public TimelineCases timelineCases;
@Inject
public MastodonApi mastodonApi;
private SwipeRefreshLayout swipeRefreshLayout; private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView; private RecyclerView recyclerView;
private ThreadAdapter adapter; private ThreadAdapter adapter;
@ -87,6 +97,11 @@ public class ViewThreadFragment extends SFragment implements
return fragment; return fragment;
} }
@Override
protected TimelineCases timelineCases() {
return timelineCases;
}
@Nullable @Nullable
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@ -161,7 +176,7 @@ public class ViewThreadFragment extends SFragment implements
@Override @Override
public void onReblog(final boolean reblog, final int position) { public void onReblog(final boolean reblog, final int position) {
final Status status = statuses.get(position); final Status status = statuses.get(position);
super.reblogWithCallback(statuses.get(position), reblog, new Callback<Status>() { timelineCases.reblogWithCallback(statuses.get(position), reblog, new Callback<Status>() {
@Override @Override
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) { public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
@ -194,7 +209,7 @@ public class ViewThreadFragment extends SFragment implements
@Override @Override
public void onFavourite(final boolean favourite, final int position) { public void onFavourite(final boolean favourite, final int position) {
final Status status = statuses.get(position); final Status status = statuses.get(position);
super.favouriteWithCallback(statuses.get(position), favourite, new Callback<Status>() { timelineCases.favouriteWithCallback(statuses.get(position), favourite, new Callback<Status>() {
@Override @Override
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) { public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
if (response.isSuccessful()) { if (response.isSuccessful()) {

View file

@ -1,42 +0,0 @@
package com.keylesspalace.tusky.network;
import android.support.annotation.NonNull;
import com.keylesspalace.tusky.TuskyApplication;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* Created by charlag on 31/10/17.
*/
public final class AuthInterceptor implements Interceptor {
AccountManager accountManager;
public AuthInterceptor(AccountManager accountManager) {
this.accountManager = accountManager;
}
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Request originalRequest = chain.request();
AccountEntity currentAccount = accountManager.getActiveAccount();
Request.Builder builder = originalRequest.newBuilder();
if (currentAccount != null) {
builder.header("Authorization", String.format("Bearer %s", currentAccount.getAccessToken()));
}
Request newRequest = builder.build();
return chain.proceed(newRequest);
}
}

View file

@ -0,0 +1,69 @@
/* 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.network;
import android.support.annotation.NonNull;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import java.io.IOException;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* Created by charlag on 31/10/17.
*/
public final class InstanceSwitchAuthInterceptor implements Interceptor {
private AccountManager accountManager;
public InstanceSwitchAuthInterceptor(AccountManager accountManager) {
this.accountManager = accountManager;
}
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Request originalRequest = chain.request();
AccountEntity currentAccount = accountManager.getActiveAccount();
Request.Builder builder = originalRequest.newBuilder();
String instanceHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER);
if (instanceHeader != null) {
// use domain explicitly specified in custom header
builder.url(swapHost(originalRequest.url(), instanceHeader));
builder.removeHeader(MastodonApi.DOMAIN_HEADER);
} else if (currentAccount != null) {
//use domain of current account
builder.url(swapHost(originalRequest.url(), currentAccount.getDomain()))
.header("Authorization",
String.format("Bearer %s", currentAccount.getAccessToken()));
}
Request newRequest = builder.build();
return chain.proceed(newRequest);
}
@NonNull
private HttpUrl swapHost(@NonNull HttpUrl url, @NonNull String host) {
return url.newBuilder().host(host).build();
}
}

View file

@ -50,6 +50,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";
@GET("api/v1/timelines/home") @GET("api/v1/timelines/home")
Call<List<Status>> homeTimeline( Call<List<Status>> homeTimeline(
@ -83,7 +84,7 @@ public interface MastodonApi {
@Query("limit") Integer limit); @Query("limit") Integer limit);
@GET("api/v1/notifications") @GET("api/v1/notifications")
Call<List<Notification>> notificationsWithAuth( Call<List<Notification>> notificationsWithAuth(
@Header("Authorization") String auth); @Header("Authorization") String auth, @Header(DOMAIN_HEADER) String domain);
@POST("api/v1/notifications/clear") @POST("api/v1/notifications/clear")
Call<ResponseBody> clearNotifications(); Call<ResponseBody> clearNotifications();
@GET("api/v1/notifications/{id}") @GET("api/v1/notifications/{id}")

View file

@ -0,0 +1,99 @@
/* 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.network
import android.content.Intent
import android.support.v4.content.LocalBroadcastManager
import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.receiver.TimelineReceiver
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
/**
* Created by charlag on 3/24/18.
*/
interface TimelineCases {
fun reblogWithCallback(status: Status, reblog: Boolean, callback: Callback<Status>)
fun favouriteWithCallback(status: Status, favourite: Boolean, callback: Callback<Status>)
fun mute(id: String)
fun block(id: String)
fun delete(id: String)
}
class TimelineCasesImpl(
private val mastodonApi: MastodonApi,
private val broadcastManager: LocalBroadcastManager
) : TimelineCases {
override fun reblogWithCallback(status: Status, reblog: Boolean, callback: Callback<Status>) {
val id = status.actionableId
val call = if (reblog) {
mastodonApi.reblogStatus(id)
} else {
mastodonApi.unreblogStatus(id)
}
call.enqueue(callback)
}
override fun favouriteWithCallback(status: Status, favourite: Boolean, callback: Callback<Status>) {
val id = status.actionableId
val call = if (favourite) {
mastodonApi.favouriteStatus(id)
} else {
mastodonApi.unfavouriteStatus(id)
}
call.enqueue(callback)
}
override fun mute(id: String) {
val call = mastodonApi.muteAccount(id)
call.enqueue(object : Callback<Relationship> {
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) {}
override fun onFailure(call: Call<Relationship>, t: Throwable) {}
})
val intent = Intent(TimelineReceiver.Types.MUTE_ACCOUNT)
intent.putExtra("id", id)
broadcastManager.sendBroadcast(intent)
}
override fun block(id: String) {
val call = mastodonApi.blockAccount(id)
call.enqueue(object : Callback<Relationship> {
override fun onResponse(call: Call<Relationship>, response: retrofit2.Response<Relationship>) {}
override fun onFailure(call: Call<Relationship>, t: Throwable) {}
})
val intent = Intent(TimelineReceiver.Types.BLOCK_ACCOUNT)
intent.putExtra("id", id)
broadcastManager.sendBroadcast(intent)
}
override fun delete(id: String) {
val call = mastodonApi.deleteStatus(id)
call.enqueue(object : Callback<ResponseBody> {
override fun onResponse(call: Call<ResponseBody>, response: retrofit2.Response<ResponseBody>) {}
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {}
})
}
}

View file

@ -47,6 +47,7 @@ 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
@ -91,6 +92,10 @@ 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);
} }

View file

@ -1,3 +1,19 @@
/* 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 package com.keylesspalace.tusky
import android.widget.EditText import android.widget.EditText
@ -51,12 +67,13 @@ class ComposeActivityTest {
fun before() { fun before() {
val controller = Robolectric.buildActivity(ComposeActivity::class.java) val controller = Robolectric.buildActivity(ComposeActivity::class.java)
activity = controller.get() activity = controller.get()
application = activity.application as FakeTuskyApplication
serviceLocator = Mockito.mock(TuskyApplication.ServiceLocator::class.java)
application.locator = serviceLocator
accountManagerMock = Mockito.mock(AccountManager::class.java) accountManagerMock = Mockito.mock(AccountManager::class.java)
serviceLocator = Mockito.mock(TuskyApplication.ServiceLocator::class.java)
`when`(serviceLocator.get(AccountManager::class.java)).thenReturn(accountManagerMock) `when`(serviceLocator.get(AccountManager::class.java)).thenReturn(accountManagerMock)
`when`(accountManagerMock.activeAccount).thenReturn(account) `when`(accountManagerMock.activeAccount).thenReturn(account)
activity.accountManager = accountManagerMock
application = activity.application as FakeTuskyApplication
application.locator = serviceLocator
controller.create().start() controller.create().start()
} }

0
gradlew vendored Executable file → Normal file
View file

0
gradlew.bat vendored Normal file → Executable file
View file