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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,63 @@
/* Copyright 2018 charlag
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.*
import dagger.Module
import dagger.android.ContributesAndroidInjector
/**
* 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.FooterViewHolder;
import com.keylesspalace.tusky.adapter.MutesAdapter;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
@ -49,11 +50,14 @@ import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import java.util.List;
import javax.inject.Inject;
import retrofit2.Call;
import retrofit2.Callback;
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
public AccountListFragment() {
@ -67,13 +71,15 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
FOLLOW_REQUESTS,
}
@Inject
public MastodonApi api;
private Type type;
private String accountId;
private LinearLayoutManager layoutManager;
private RecyclerView recyclerView;
private EndlessOnScrollListener scrollListener;
private AccountAdapter adapter;
private MastodonApi api;
private boolean bottomLoading;
private int bottomFetches;
private boolean topLoading;
@ -146,12 +152,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
@Override
public void onActivityCreated(@Nullable Bundle 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.
scrollListener = new EndlessOnScrollListener(layoutManager) {
@Override
@ -464,7 +464,7 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
private void onLoadMore(RecyclerView recyclerView) {
AccountAdapter adapter = (AccountAdapter) recyclerView.getAdapter();
//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);
}

View file

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

View file

@ -15,14 +15,10 @@
package com.keylesspalace.tusky.fragment;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import com.keylesspalace.tusky.R;
import java.util.ArrayList;
import java.util.List;
@ -44,9 +40,4 @@ public class BaseFragment extends Fragment {
}
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.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
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.util.CollectionUtil;
import com.keylesspalace.tusky.util.Either;
@ -64,14 +67,18 @@ import java.math.BigInteger;
import java.util.Iterator;
import java.util.List;
import javax.inject.Inject;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class NotificationsFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener,
SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
NotificationsAdapter.NotificationActionListener,
SharedPreferences.OnSharedPreferenceChangeListener {
SharedPreferences.OnSharedPreferenceChangeListener,
Injectable {
private static final String TAG = "NotificationF"; // logging tag
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 LinearLayoutManager layoutManager;
private RecyclerView recyclerView;
@ -113,6 +125,11 @@ public class NotificationsFragment extends SFragment implements
private String topId;
private boolean alwaysShowSensitiveMedia;
@Override
protected TimelineCases timelineCases() {
return timelineCases;
}
// Each element is either a Notification for loading data or a Placeholder
private final PairedList<Either<Placeholder, Notification>, NotificationViewData> notifications
= 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) {
final Notification notification = notifications.get(position).getAsRight();
final Status status = notification.getStatus();
reblogWithCallback(status, reblog, new Callback<Status>() {
timelineCases.reblogWithCallback(status, reblog, new Callback<Status>() {
@Override
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
if (response.isSuccessful()) {
@ -310,7 +327,7 @@ public class NotificationsFragment extends SFragment implements
public void onFavourite(final boolean favourite, final int position) {
final Notification notification = notifications.get(position).getAsRight();
final Status status = notification.getStatus();
favouriteWithCallback(status, favourite, new Callback<Status>() {
timelineCases.favouriteWithCallback(status, favourite, new Callback<Status>() {
@Override
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
if (response.isSuccessful()) {

View file

@ -41,18 +41,22 @@ import com.keylesspalace.tusky.ViewTagActivity;
import com.keylesspalace.tusky.ViewThreadActivity;
import com.keylesspalace.tusky.ViewVideoActivity;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases;
import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.HtmlUtils;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
@ -69,7 +73,8 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
protected String loggedInAccountId;
protected String loggedInUsername;
protected MastodonApi mastodonApi;
protected abstract TimelineCases timelineCases();
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
@ -80,8 +85,6 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
loggedInAccountId = activeAccount.getAccountId();
loggedInUsername = activeAccount.getUsername();
}
BaseActivity activity = (BaseActivity) getActivity();
mastodonApi = activity.mastodonApi;
}
@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);
}
protected void openReblog(@Nullable final Status status) {
if (status == null) return;
viewAccount(status.getAccount().getId());
}
protected void reply(Status status) {
String inReplyToId = status.getActionableId();
Status actionableStatus = status.getActionableStatus();
@ -113,88 +121,6 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
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) {
final String id = status.getActionableId();
final String accountId = status.getActionableStatus().getAccount().getId();
@ -208,60 +134,56 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
} else {
popup.inflate(R.menu.status_more_for_user);
}
popup.setOnMenuItemClickListener(
new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.status_share_content: {
StringBuilder sb = new StringBuilder();
sb.append(status.getAccount().getUsername());
sb.append(" - ");
sb.append(status.getContent().toString());
popup.setOnMenuItemClickListener(item -> {
switch (item.getItemId()) {
case R.id.status_share_content: {
StringBuilder sb = new StringBuilder();
sb.append(status.getAccount().getUsername());
sb.append(" - ");
sb.append(status.getContent().toString());
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, sb.toString());
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to)));
return true;
}
case R.id.status_share_link: {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl);
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_link_to)));
return true;
}
case R.id.status_copy_link: {
ClipboardManager clipboard = (ClipboardManager)
getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(null, statusUrl);
clipboard.setPrimaryClip(clip);
return true;
}
case R.id.status_mute: {
mute(accountId);
return true;
}
case R.id.status_block: {
block(accountId);
return true;
}
case R.id.status_report: {
openReportPage(accountId, accountUsename, id, content);
return true;
}
case R.id.status_delete: {
delete(id);
removeItem(position);
return true;
}
}
return false;
}
});
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, sb.toString());
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to)));
return true;
}
case R.id.status_share_link: {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl);
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_link_to)));
return true;
}
case R.id.status_copy_link: {
ClipboardManager clipboard = (ClipboardManager)
getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(null, statusUrl);
clipboard.setPrimaryClip(clip);
return true;
}
case R.id.status_mute: {
timelineCases().mute(accountId);
return true;
}
case R.id.status_block: {
timelineCases().block(accountId);
return true;
}
case R.id.status_report: {
openReportPage(accountId, accountUsename, id, content);
return true;
}
case R.id.status_delete: {
timelineCases().delete(id);
removeItem(position);
return true;
}
}
return false;
});
popup.show();
}

View file

@ -40,11 +40,13 @@ import com.keylesspalace.tusky.BuildConfig;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.adapter.FooterViewHolder;
import com.keylesspalace.tusky.adapter.TimelineAdapter;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
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.util.CollectionUtil;
import com.keylesspalace.tusky.util.Either;
@ -60,6 +62,8 @@ import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -67,7 +71,8 @@ import retrofit2.Response;
public class TimelineFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
SharedPreferences.OnSharedPreferenceChangeListener {
SharedPreferences.OnSharedPreferenceChangeListener,
Injectable {
private static final String TAG = "TimelineF"; // logging tag
private static final String KIND_ARG = "kind";
private static final String HASHTAG_OR_ID_ARG = "hashtag_or_id";
@ -90,6 +95,11 @@ public class TimelineFragment extends SFragment implements
MIDDLE
}
@Inject
TimelineCases timelineCases;
@Inject
MastodonApi mastodonApi;
private SwipeRefreshLayout swipeRefreshLayout;
private TimelineAdapter adapter;
private Kind kind;
@ -113,6 +123,11 @@ public class TimelineFragment extends SFragment implements
private boolean alwaysShowSensitiveMedia;
@Override
protected TimelineCases timelineCases() {
return timelineCases;
}
private PairedList<Either<Placeholder, Status>, StatusViewData> statuses =
new PairedList<>(new Function<Either<Placeholder, Status>, StatusViewData>() {
@Override
@ -307,7 +322,7 @@ public class TimelineFragment extends SFragment implements
@Override
public void onReblog(final boolean reblog, final int position) {
final Status status = statuses.get(position).getAsRight();
super.reblogWithCallback(status, reblog, new Callback<Status>() {
timelineCases.reblogWithCallback(status, reblog, new Callback<Status>() {
@Override
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) {
final Status status = statuses.get(position).getAsRight();
super.favouriteWithCallback(status, favourite, new Callback<Status>() {
timelineCases.favouriteWithCallback(status, favourite, new Callback<Status>() {
@Override
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.R;
import com.keylesspalace.tusky.adapter.ThreadAdapter;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StatusContext;
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.util.PairedList;
import com.keylesspalace.tusky.util.ThemeUtils;
@ -53,14 +56,21 @@ import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class ViewThreadFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener {
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, Injectable {
private static final String TAG = "ViewThreadFragment";
@Inject
public TimelineCases timelineCases;
@Inject
public MastodonApi mastodonApi;
private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView;
private ThreadAdapter adapter;
@ -87,6 +97,11 @@ public class ViewThreadFragment extends SFragment implements
return fragment;
}
@Override
protected TimelineCases timelineCases() {
return timelineCases;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@ -161,7 +176,7 @@ public class ViewThreadFragment extends SFragment implements
@Override
public void onReblog(final boolean reblog, final int 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
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
if (response.isSuccessful()) {
@ -194,7 +209,7 @@ public class ViewThreadFragment extends SFragment implements
@Override
public void onFavourite(final boolean favourite, final int 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
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
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 {
String ENDPOINT_AUTHORIZE = "/oauth/authorize";
String DOMAIN_HEADER = "domain";
@GET("api/v1/timelines/home")
Call<List<Status>> homeTimeline(
@ -83,7 +84,7 @@ public interface MastodonApi {
@Query("limit") Integer limit);
@GET("api/v1/notifications")
Call<List<Notification>> notificationsWithAuth(
@Header("Authorization") String auth);
@Header("Authorization") String auth, @Header(DOMAIN_HEADER) String domain);
@POST("api/v1/notifications/clear")
Call<ResponseBody> clearNotifications();
@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.OkHttpClient;
import okhttp3.Request;
import okhttp3.logging.HttpLoggingInterceptor;
public class OkHttpUtils {
private static final String TAG = "OkHttpUtils"; // logging tag
@ -91,6 +92,10 @@ public class OkHttpUtils {
builder.proxy(new Proxy(Proxy.Type.HTTP, address));
}
if(BuildConfig.DEBUG) {
builder.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC));
}
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
import android.widget.EditText
@ -51,12 +67,13 @@ class ComposeActivityTest {
fun before() {
val controller = Robolectric.buildActivity(ComposeActivity::class.java)
activity = controller.get()
application = activity.application as FakeTuskyApplication
serviceLocator = Mockito.mock(TuskyApplication.ServiceLocator::class.java)
application.locator = serviceLocator
accountManagerMock = Mockito.mock(AccountManager::class.java)
serviceLocator = Mockito.mock(TuskyApplication.ServiceLocator::class.java)
`when`(serviceLocator.get(AccountManager::class.java)).thenReturn(accountManagerMock)
`when`(accountManagerMock.activeAccount).thenReturn(account)
activity.accountManager = accountManagerMock
application = activity.application as FakeTuskyApplication
application.locator = serviceLocator
controller.create().start()
}

0
gradlew vendored Executable file → Normal file
View file

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