diff --git a/README.md b/README.md index 2ab51d89..8bc62e88 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ Tusky is a beautiful Android client for [Mastodon](https://github.com/tootsuite/mastodon). Mastodon is a GNU social-compatible federated social network. That means not one entity controls the whole network, rather, like e-mail, volunteers and organisations operate their own independent servers, users from which can all interact with each other seamlessly. -It is currently available on [Google Play](https://play.google.com/store/apps/details?id=com.keylesspalace.tusky). - ## Features - Material Design @@ -16,6 +14,10 @@ It is currently available on [Google Play](https://play.google.com/store/apps/de My Mastodon account is [Vavassor@mastodon.social](https://mastodon.social/users/Vavassor). +[Get it on F-Droid](https://f-droid.org/repository/browse/?fdid=com.keylesspalace.tusky) +[Get it on Google Play](https://play.google.com/store/apps/details?id=com.keylesspalace.tusky&utm_source=github&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1) +[![Available at Amazon](/assets/amazon_badge.png)](https://www.amazon.com/gp/product/B06ZYXT88G/ref=mas_pm_tusky) + ## Building The most basic things needed are the Java Development Kit 7 or higher and the Android SDK. diff --git a/app/.gitignore b/app/.gitignore index 3f1ce47a..d4026ab8 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,2 +1,4 @@ /build app-release.apk +app-google-release.apk +src/main/res/raw/keystore_tusky_api.bks diff --git a/app/build.gradle b/app/build.gradle index a7b9f5ae..6453f7c5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,24 +2,16 @@ apply plugin: 'com.android.application' android { compileSdkVersion 25 - buildToolsVersion "25.0.2" + buildToolsVersion "25.0.3" defaultConfig { applicationId "com.keylesspalace.tusky" minSdkVersion 15 targetSdkVersion 25 - versionCode 15 - versionName "1.1.2" + versionCode 17 + versionName "1.1.4-beta.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary true } - productFlavors { - google { - buildConfigField "boolean", "USES_PUSH_NOTIFICATIONS", "true" - } - fdroid { - buildConfigField "boolean", "USES_PUSH_NOTIFICATIONS", "false" - } - } buildTypes { release { minifyEnabled true @@ -37,7 +29,7 @@ dependencies { androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) - compile('com.mikepenz:materialdrawer:5.8.2@aar') { + compile('com.mikepenz:materialdrawer:5.9.1@aar') { transitive = true } compile 'com.android.support:appcompat-v7:25.3.1' @@ -47,22 +39,22 @@ dependencies { compile 'com.android.support:design:25.3.1' compile 'com.android.support:exifinterface:25.3.1' compile 'com.squareup.retrofit2:retrofit:2.2.0' - compile 'com.squareup.retrofit2:converter-gson:2.1.0' + compile 'com.squareup.retrofit2:converter-gson:2.2.0' compile 'com.squareup.picasso:picasso:2.5.2' compile 'com.squareup.okhttp3:okhttp:3.7.0' compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0' compile 'com.pkmmte.view:circularimageview:1.1' - compile 'com.github.peter9870:sparkbutton:master' + compile 'com.github.varunest:sparkbutton:1.0.5' compile 'com.mikhaellopez:circularfillableloaders:1.2.0' - compile 'com.github.chrisbanes:PhotoView:1.3.1' + compile 'com.github.chrisbanes:PhotoView:2.0.0' compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' compile 'com.github.arimorty:floatingsearchview:2.0.4' - compile 'com.theartofdev.edmodo:android-image-cropper:2.4.0' - compile 'com.jakewharton:butterknife:8.4.0' - googleCompile 'com.google.firebase:firebase-messaging:10.0.1' - googleCompile 'com.google.firebase:firebase-crash:10.0.1' + compile 'com.theartofdev.edmodo:android-image-cropper:2.4.3' + compile 'com.jakewharton:butterknife:8.5.1' + compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0' + compile('org.eclipse.paho:org.eclipse.paho.android.service:1.1.1') { + exclude module: 'support-v4' + } testCompile 'junit:junit:4.12' - annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0' + annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1' } - -apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json deleted file mode 100644 index bfd19f88..00000000 --- a/app/google-services.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "project_info": { - "project_number": "268851337880", - "firebase_url": "https://tusky-62772.firebaseio.com", - "project_id": "tusky-62772", - "storage_bucket": "tusky-62772.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:268851337880:android:fc4111b1d145a00e", - "android_client_info": { - "package_name": "com.keylesspalace.tusky" - } - }, - "oauth_client": [ - { - "client_id": "268851337880-eie2ssto2d21bfihn9d1qupcrke8oebf.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "com.keylesspalace.tusky", - "certificate_hash": "18d196307d6e928e99c2e0bb9818c01c38aff2f9" - } - }, - { - "client_id": "268851337880-n19d05m282nirs1fc9kdd5n4of6je4fk.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyCbJtSjuk4I3Jy8PdUaO3TaQOXubcOUElo" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 2, - "other_platform_oauth_client": [ - { - "client_id": "268851337880-n19d05m282nirs1fc9kdd5n4of6je4fk.apps.googleusercontent.com", - "client_type": 3 - } - ] - }, - "ads_service": { - "status": 2 - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/app/src/fdroid/AndroidManifest.xml b/app/src/fdroid/AndroidManifest.xml deleted file mode 100644 index b7060e6e..00000000 --- a/app/src/fdroid/AndroidManifest.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/fdroid/java/com/keylesspalace/tusky/MessagingService.java b/app/src/fdroid/java/com/keylesspalace/tusky/MessagingService.java deleted file mode 100644 index 01d98122..00000000 --- a/app/src/fdroid/java/com/keylesspalace/tusky/MessagingService.java +++ /dev/null @@ -1,132 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky; - -import android.app.IntentService; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.text.Spanned; -import android.util.ArraySet; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import com.keylesspalace.tusky.entity.Notification; - -import java.util.HashSet; -import java.util.List; - -import java.io.IOException; -import java.util.Set; - -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; - -public class MessagingService extends IntentService { - public static final int NOTIFY_ID = 6; // This is an arbitrary number. - - private MastodonAPI mastodonAPI; - - public MessagingService() { - super("Tusky Pull Notification Service"); - } - - @Override - protected void onHandleIntent(Intent intent) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences( - getApplicationContext()); - boolean enabled = preferences.getBoolean("notificationsEnabled", true); - if (!enabled) { - return; - } - - createMastodonApi(); - - mastodonAPI.notifications(null, null, null).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, - Response> response) { - if (response.isSuccessful()) { - onNotificationsReceived(response.body()); - } - } - - @Override - public void onFailure(Call> call, Throwable t) {} - }); - } - - private void createMastodonApi() { - SharedPreferences preferences = getSharedPreferences( - getString(R.string.preferences_file_key), Context.MODE_PRIVATE); - final String domain = preferences.getString("domain", null); - final String accessToken = preferences.getString("accessToken", null); - - OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder() - .addInterceptor(new Interceptor() { - @Override - public okhttp3.Response intercept(Chain chain) throws IOException { - Request originalRequest = chain.request(); - - Request.Builder builder = originalRequest.newBuilder() - .header("Authorization", String.format("Bearer %s", accessToken)); - - Request newRequest = builder.build(); - - return chain.proceed(newRequest); - } - }) - .build(); - - Gson gson = new GsonBuilder() - .registerTypeAdapter(Spanned.class, new SpannedTypeAdapter()) - .registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter()) - .create(); - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl("https://" + domain) - .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create(gson)) - .build(); - - mastodonAPI = retrofit.create(MastodonAPI.class); - } - - private void onNotificationsReceived(List notificationList) { - SharedPreferences notificationsPreferences = getSharedPreferences( - "Notifications", Context.MODE_PRIVATE); - Set currentIds = notificationsPreferences.getStringSet( - "current_ids", new HashSet()); - for (Notification notification : notificationList) { - String id = notification.id; - if (!currentIds.contains(id)) { - currentIds.add(id); - NotificationMaker.make(this, NOTIFY_ID, notification); - } - } - notificationsPreferences.edit() - .putStringSet("current_ids", currentIds) - .apply(); - } -} diff --git a/app/src/google/AndroidManifest.xml b/app/src/google/AndroidManifest.xml deleted file mode 100644 index 20ecbe94..00000000 --- a/app/src/google/AndroidManifest.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/google/java/com/keylesspalace/tusky/MessagingService.java b/app/src/google/java/com/keylesspalace/tusky/MessagingService.java deleted file mode 100644 index cf4e5273..00000000 --- a/app/src/google/java/com/keylesspalace/tusky/MessagingService.java +++ /dev/null @@ -1,121 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . - * - * If you modify this Program, or any covered work, by linking or combining it with Firebase Cloud - * Messaging and Firebase Crash Reporting (or a modified version of those libraries), containing - * parts covered by the Google APIs Terms of Service, the licensors of this Program grant you - * additional permission to convey the resulting work. */ - -package com.keylesspalace.tusky; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.text.Spanned; - -import com.google.firebase.messaging.FirebaseMessagingService; -import com.google.firebase.messaging.RemoteMessage; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import com.keylesspalace.tusky.entity.Notification; - -import java.io.IOException; - -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; - -public class MessagingService extends FirebaseMessagingService { - private MastodonAPI mastodonAPI; - private static final String TAG = "MessagingService"; - public static final int NOTIFY_ID = 666; - - @Override - public void onMessageReceived(RemoteMessage remoteMessage) { - Log.d(TAG, remoteMessage.getFrom()); - Log.d(TAG, remoteMessage.toString()); - - String notificationId = remoteMessage.getData().get("notification_id"); - - if (notificationId == null) { - Log.e(TAG, "No notification ID in payload!!"); - return; - } - - Log.d(TAG, notificationId); - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences( - getApplicationContext()); - boolean enabled = preferences.getBoolean("notificationsEnabled", true); - if (!enabled) { - return; - } - - createMastodonAPI(); - - mastodonAPI.notification(notificationId).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - NotificationMaker.make(MessagingService.this, NOTIFY_ID, response.body()); - } - } - - @Override - public void onFailure(Call call, Throwable t) {} - }); - } - - private void createMastodonAPI() { - SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE); - final String domain = preferences.getString("domain", null); - final String accessToken = preferences.getString("accessToken", null); - - OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder() - .addInterceptor(new Interceptor() { - @Override - public okhttp3.Response intercept(Chain chain) throws IOException { - Request originalRequest = chain.request(); - - Request.Builder builder = originalRequest.newBuilder() - .header("Authorization", String.format("Bearer %s", accessToken)); - - Request newRequest = builder.build(); - - return chain.proceed(newRequest); - } - }) - .build(); - - Gson gson = new GsonBuilder() - .registerTypeAdapter(Spanned.class, new SpannedTypeAdapter()) - .registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter()) - .create(); - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl("https://" + domain) - .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create(gson)) - .build(); - - mastodonAPI = retrofit.create(MastodonAPI.class); - } -} diff --git a/app/src/google/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java b/app/src/google/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java deleted file mode 100644 index adb47879..00000000 --- a/app/src/google/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java +++ /dev/null @@ -1,83 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . - * - * If you modify this Program, or any covered work, by linking or combining it with Firebase Cloud - * Messaging and Firebase Crash Reporting (or a modified version of those libraries), containing - * parts covered by the Google APIs Terms of Service, the licensors of this Program grant you - * additional permission to convey the resulting work. */ - -package com.keylesspalace.tusky; - -import android.content.Context; -import android.content.SharedPreferences; - -import com.google.firebase.iid.FirebaseInstanceId; -import com.google.firebase.iid.FirebaseInstanceIdService; - -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; -import retrofit2.Retrofit; - -public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService { - private static final String TAG = "com.keylesspalace.tusky.MyFirebaseInstanceIdService"; - - private TuskyAPI tuskyAPI; - - protected void createTuskyAPI() { - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(getString(R.string.tusky_api_url)) - .client(OkHttpUtils.getCompatibleClient()) - .build(); - - tuskyAPI = retrofit.create(TuskyAPI.class); - } - - @Override - public void onTokenRefresh() { - createTuskyAPI(); - - String refreshedToken = FirebaseInstanceId.getInstance().getToken(); - SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE); - String accessToken = preferences.getString("accessToken", null); - String domain = preferences.getString("domain", null); - - if (accessToken != null && domain != null) { - tuskyAPI.unregister("https://" + domain, accessToken).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - Log.d(TAG, response.message()); - } - - @Override - public void onFailure(Call call, Throwable t) { - Log.d(TAG, t.getMessage()); - } - }); - tuskyAPI.register("https://" + domain, accessToken, refreshedToken).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - Log.d(TAG, response.message()); - } - - @Override - public void onFailure(Call call, Throwable t) { - Log.d(TAG, t.getMessage()); - } - }); - } - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 84c1b093..c30a26ca 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,20 +6,23 @@ - + + + + android:theme="@style/AppTheme"> - + - @@ -37,7 +40,10 @@ android:scheme="@string/oauth_scheme" /> - + + @@ -57,14 +63,19 @@ - - + + + @@ -72,11 +83,12 @@ android:name="com.theartofdev.edmodo.cropper.CropImageActivity" android:theme="@style/Base.Theme.AppCompat" /> - + + diff --git a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.java b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.java new file mode 100644 index 00000000..01e59066 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.java @@ -0,0 +1,108 @@ +package com.keylesspalace.tusky; + +import android.content.Intent; +import android.os.Bundle; +import android.support.design.widget.Snackbar; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import com.keylesspalace.tusky.entity.Account; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class AboutActivity extends BaseActivity { + private Button appAccountButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_about); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar bar = getSupportActionBar(); + if (bar != null) { + bar.setDisplayHomeAsUpEnabled(true); + bar.setDisplayShowHomeEnabled(true); + } + + TextView versionTextView = (TextView) findViewById(R.id.versionTV); + String versionName = BuildConfig.VERSION_NAME; + String versionFormat = getString(R.string.about_application_version); + versionTextView.setText(String.format(versionFormat, versionName)); + + appAccountButton = (Button) findViewById(R.id.tusky_profile_button); + appAccountButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onAccountButtonClick(); + } + }); + } + + private void onAccountButtonClick() { + String appAccountId = getPrivatePreferences().getString("appAccountId", null); + if (appAccountId != null) { + viewAccount(appAccountId); + } else { + searchForAccountThenViewIt(); + } + } + + private void viewAccount(String id) { + Intent intent = new Intent(this, AccountActivity.class); + intent.putExtra("id", id); + startActivity(intent); + } + + private void searchForAccountThenViewIt() { + Callback> callback = new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful()) { + List accountList = response.body(); + if (!accountList.isEmpty()) { + String id = accountList.get(0).id; + getPrivatePreferences().edit() + .putString("appAccountId", id) + .apply(); + viewAccount(id); + } else { + onSearchFailed(); + } + } else { + onSearchFailed(); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + onSearchFailed(); + } + }; + mastodonAPI.searchAccounts("Tusky@mastodon.social", true, null).enqueue(callback); + } + + private void onSearchFailed() { + Snackbar.make(appAccountButton, R.string.error_generic, Snackbar.LENGTH_LONG).show(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: { + onBackPressed(); + return true; + } + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java index ed12bd2c..ae6f99ac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java @@ -23,6 +23,7 @@ import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; +import android.preference.PreferenceManager; import android.support.annotation.AttrRes; import android.support.annotation.Nullable; import android.support.design.widget.AppBarLayout; @@ -31,6 +32,7 @@ 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; import android.support.v7.app.ActionBar; @@ -43,6 +45,15 @@ import android.widget.TextView; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Relationship; +import com.keylesspalace.tusky.fragment.SFragment; +import com.keylesspalace.tusky.interfaces.LinkListener; +import com.keylesspalace.tusky.interfaces.StatusRemoveListener; +import com.keylesspalace.tusky.pager.AccountPagerAdapter; +import com.keylesspalace.tusky.util.LinkHelper; +import com.keylesspalace.tusky.util.Assert; +import com.keylesspalace.tusky.util.Log; +import com.keylesspalace.tusky.util.TimelineReceiver; +import com.keylesspalace.tusky.util.ThemeUtils; import com.pkmmte.view.CircularImageView; import com.squareup.picasso.Picasso; @@ -156,9 +167,6 @@ public class AccountActivity extends BaseActivity implements SFragment.OnUserRem // Initialise the default UI states. floatingBtn.hide(); - avatar.setImageResource(R.drawable.avatar_default); - header.setImageResource(R.drawable.account_header_default); - // Obtain information to fill out the profile. obtainAccount(); if (!accountId.equals(loggedInAccountId)) { @@ -237,7 +245,9 @@ public class AccountActivity extends BaseActivity implements SFragment.OnUserRem displayName.setText(account.getDisplayName()); - LinkHelper.setClickableText(note, account.note, null, new LinkListener() { + boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean("customTabs", true); + LinkHelper.setClickableText(note, account.note, null, useCustomTabs, new LinkListener() { @Override public void onViewTag(String tag) { Intent intent = new Intent(AccountActivity.this, ViewTagActivity.class); @@ -266,7 +276,7 @@ public class AccountActivity extends BaseActivity implements SFragment.OnUserRem .into(avatar); Picasso.with(this) .load(account.header) - .placeholder(R.drawable.account_header_missing) + .placeholder(R.drawable.account_header_default) .into(header); NumberFormat nf = NumberFormat.getInstance(); @@ -459,6 +469,7 @@ public class AccountActivity extends BaseActivity implements SFragment.OnUserRem Snackbar.LENGTH_LONG).show(); } else { followState = FollowState.NOT_FOLLOWING; + broadcast(TimelineReceiver.Types.UNFOLLOW_ACCOUNT, id); } updateButtons(); } else { @@ -509,6 +520,7 @@ public class AccountActivity extends BaseActivity implements SFragment.OnUserRem @Override public void onResponse(Call call, Response response) { if (response.isSuccessful()) { + broadcast(TimelineReceiver.Types.BLOCK_ACCOUNT, id); blocking = response.body().blocking; updateButtons(); } else { @@ -546,6 +558,7 @@ public class AccountActivity extends BaseActivity implements SFragment.OnUserRem @Override public void onResponse(Call call, Response response) { if (response.isSuccessful()) { + broadcast(TimelineReceiver.Types.MUTE_ACCOUNT, id); muting = response.body().muting; updateButtons(); } else { @@ -578,6 +591,11 @@ public class AccountActivity extends BaseActivity implements SFragment.OnUserRem .show(); } + private void broadcast(String action, String id) { + Intent intent = new Intent(action); + intent.putExtra("id", id); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); + } @Override public boolean onOptionsItemSelected(MenuItem item) { diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java index 1d1276fb..86c5a05c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java @@ -24,6 +24,8 @@ import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.view.MenuItem; +import com.keylesspalace.tusky.fragment.AccountListFragment; + public class AccountListActivity extends BaseActivity { enum Type { BLOCKS, diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index 5bbe7ba1..fa569fa8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -15,8 +15,6 @@ package com.keylesspalace.tusky; -import android.app.AlarmManager; -import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -24,7 +22,6 @@ import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; @@ -34,6 +31,15 @@ import android.view.Menu; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.keylesspalace.tusky.entity.Session; +import com.keylesspalace.tusky.json.SpannedTypeAdapter; +import com.keylesspalace.tusky.json.StringWithEmoji; +import com.keylesspalace.tusky.json.StringWithEmojiTypeAdapter; +import com.keylesspalace.tusky.network.MastodonAPI; +import com.keylesspalace.tusky.network.TuskyApi; +import com.keylesspalace.tusky.util.Log; +import com.keylesspalace.tusky.util.OkHttpUtils; +import com.keylesspalace.tusky.util.PushNotificationClient; import java.io.IOException; @@ -51,10 +57,10 @@ import retrofit2.converter.gson.GsonConverterFactory; public class BaseActivity extends AppCompatActivity { private static final String TAG = "BaseActivity"; // logging tag - protected MastodonAPI mastodonAPI; - protected TuskyAPI tuskyAPI; + public MastodonAPI mastodonAPI; + public TuskyApi tuskyApi; + protected PushNotificationClient pushNotificationClient; protected Dispatcher mastodonApiDispatcher; - protected PendingIntent serviceAlarmIntent; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -62,7 +68,8 @@ public class BaseActivity extends AppCompatActivity { redirectIfNotLoggedIn(); createMastodonAPI(); - createTuskyAPI(); + createTuskyApi(); + createPushNotificationClient(); /* There isn't presently a way to globally change the theme of a whole application at * runtime, just individual activities. So, each activity has to set its theme before any @@ -154,15 +161,19 @@ public class BaseActivity extends AppCompatActivity { mastodonAPI = retrofit.create(MastodonAPI.class); } - protected void createTuskyAPI() { - if (BuildConfig.USES_PUSH_NOTIFICATIONS) { - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(getString(R.string.tusky_api_url)) - .client(OkHttpUtils.getCompatibleClient()) - .build(); + protected void createTuskyApi() { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + getString(R.string.tusky_api_url)) + .client(OkHttpUtils.getCompatibleClient()) + .addConverterFactory(GsonConverterFactory.create()) + .build(); - tuskyAPI = retrofit.create(TuskyAPI.class); - } + tuskyApi = retrofit.create(TuskyApi.class); + } + + protected void createPushNotificationClient() { + pushNotificationClient = new PushNotificationClient(getApplicationContext(), + "ssl://" + getString(R.string.tusky_api_url) + ":8883"); } protected void redirectIfNotLoggedIn() { @@ -196,49 +207,66 @@ public class BaseActivity extends AppCompatActivity { } protected void enablePushNotifications() { - if (BuildConfig.USES_PUSH_NOTIFICATIONS) { - String token = com.google.firebase.iid.FirebaseInstanceId.getInstance().getToken(); - tuskyAPI.register(getBaseUrl(), getAccessToken(), token).enqueue(new Callback() { - @Override - public void onResponse(Call call, retrofit2.Response response) { - Log.d(TAG, "Enable push notifications response: " + response.message()); + Callback callback = new Callback() { + @Override + public void onResponse(Call call, + retrofit2.Response response) { + if (response.isSuccessful()) { + pushNotificationClient.subscribeToTopic(getPushNotificationTopic()); + pushNotificationClient.connect(BaseActivity.this); + } else { + onEnablePushNotificationsFailure(response.message()); } + } - @Override - public void onFailure(Call call, Throwable t) { - Log.d(TAG, "Enable push notifications failed: " + t.getMessage()); - } - }); - } else { - // Start up the MessagingService on a repeating interval for "pull" notifications. - long checkInterval = 60 * 1000 * 5; - AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - Intent intent = new Intent(this, MessagingService.class); - final int SERVICE_REQUEST_CODE = 8574603; // This number is arbitrary. - serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE, intent, - PendingIntent.FLAG_UPDATE_CURRENT); - alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime(), checkInterval, serviceAlarmIntent); - } + @Override + public void onFailure(Call call, Throwable t) { + onEnablePushNotificationsFailure(t.getMessage()); + } + }; + String deviceToken = pushNotificationClient.getDeviceToken(); + Session session = new Session(getDomain(), getAccessToken(), deviceToken); + tuskyApi.register(session) + .enqueue(callback); + } + + private void onEnablePushNotificationsFailure(String message) { + Log.e(TAG, "Enabling push notifications failed. " + message); } protected void disablePushNotifications() { - if (BuildConfig.USES_PUSH_NOTIFICATIONS) { - tuskyAPI.unregister(getBaseUrl(), getAccessToken()).enqueue(new Callback() { - @Override - public void onResponse(Call call, retrofit2.Response response) { - Log.d(TAG, "Disable push notifications response: " + response.message()); + Callback callback = new Callback() { + @Override + public void onResponse(Call call, + retrofit2.Response response) { + if (response.isSuccessful()) { + pushNotificationClient.unsubscribeToTopic(getPushNotificationTopic()); + } else { + onDisablePushNotificationsFailure(); } + } - @Override - public void onFailure(Call call, Throwable t) { - Log.d(TAG, "Disable push notifications failed: " + t.getMessage()); - } - }); - } else if (serviceAlarmIntent != null) { - // Cancel the repeating call for "pull" notifications. - AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - alarmManager.cancel(serviceAlarmIntent); - } + @Override + public void onFailure(Call call, Throwable t) { + onDisablePushNotificationsFailure(); + } + }; + String deviceToken = pushNotificationClient.getDeviceToken(); + Session session = new Session(getDomain(), getAccessToken(), deviceToken); + tuskyApi.unregister(session) + .enqueue(callback); + } + + private void onDisablePushNotificationsFailure() { + Log.e(TAG, "Disabling push notifications failed."); + } + + private String getPushNotificationTopic() { + return String.format("%s/%s/#", getDomain(), getAccessToken()); + } + + private String getDomain() { + return getPrivatePreferences() + .getString("domain", null); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index 4efa93a2..319257b4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -54,7 +54,10 @@ import android.support.v7.app.ActionBar; import android.support.v7.content.res.AppCompatResources; import android.support.v7.widget.Toolbar; import android.text.Editable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.TextWatcher; +import android.text.style.URLSpan; import android.view.MenuItem; import android.view.View; import android.webkit.MimeTypeMap; @@ -68,6 +71,14 @@ import android.widget.TextView; import com.keylesspalace.tusky.entity.Media; import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.fragment.ComposeOptionsFragment; +import com.keylesspalace.tusky.util.DownsizeImageTask; +import com.keylesspalace.tusky.util.EditTextTyped; +import com.keylesspalace.tusky.util.CountUpDownLatch; +import com.keylesspalace.tusky.util.IOUtils; +import com.keylesspalace.tusky.util.Log; +import com.keylesspalace.tusky.util.SpanUtils; +import com.keylesspalace.tusky.util.ThemeUtils; import java.io.ByteArrayOutputStream; import java.io.File; @@ -90,6 +101,7 @@ import okhttp3.MultipartBody; import okhttp3.RequestBody; import retrofit2.Call; import retrofit2.Callback; +import retrofit2.Response; public class ComposeActivity extends BaseActivity implements ComposeOptionsFragment.Listener { private static final String TAG = "ComposeActivity"; // logging tag @@ -115,7 +127,8 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag private Uri photoUploadUri; // this only exists when a status is trying to be sent, but uploads are still occurring private ProgressDialog finishingUploadDialog; - @BindView(R.id.compose_edit_field) EditTextTyped textEditor; + @BindView(R.id.compose_edit_field) + EditTextTyped textEditor; @BindView(R.id.compose_media_preview_bar) LinearLayout mediaPreviewBar; @BindView(R.id.compose_content_warning_bar) View contentWarningBar; @BindView(R.id.field_content_warning) EditText contentWarningEditor; @@ -143,6 +156,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag Uri uri; String id; Call uploadRequest; + URLSpan uploadUrl; ReadyStage readyStage; byte[] content; long mediaSize; @@ -224,7 +238,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag floatingBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - prepareStatus(); + onSendClicked(); } }); pickBtn.setOnClickListener(new View.OnClickListener() { @@ -293,7 +307,9 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag if (replyVisibility != null && startingVisibility != null) { // Lowest possible visibility setting in response - if (startingVisibility.equals("private") || replyVisibility.equals("private")) { + if (startingVisibility.equals("direct") || replyVisibility.equals("direct")) { + startingVisibility = "direct"; + } else if (startingVisibility.equals("private") || replyVisibility.equals("private")) { startingVisibility = "private"; } else if (startingVisibility.equals("unlisted") || replyVisibility.equals("unlisted")) { startingVisibility = "unlisted"; @@ -584,24 +600,12 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag enableButtons(); } - private void prepareStatus() { + private void onSendClicked() { if (statusAlreadyInFlight) { return; } - String contentText = textEditor.getText().toString(); - String spoilerText = ""; - if (statusHideText) { - spoilerText = contentWarningEditor.getText().toString(); - } - int characterCount = contentText.length() + spoilerText.length(); - if (characterCount > 0 && characterCount <= STATUS_CHARACTER_LIMIT) { - setStateToReadying(); - readyStatus(contentText, statusVisibility, statusMarkSensitive, spoilerText); - } else if (characterCount <= 0) { - textEditor.setError(getString(R.string.error_empty)); - } else { - textEditor.setError(getString(R.string.error_compose_character_limit)); - } + setStateToReadying(); + readyStatus(statusVisibility, statusMarkSensitive); } @Override @@ -705,7 +709,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag mastodonAPI.createStatus(content, inReplyToId, spoilerText, visibility, sensitive, mediaIds).enqueue(new Callback() { @Override - public void onResponse(Call call, retrofit2.Response response) { + public void onResponse(Call call, Response response) { if (response.isSuccessful()) { onSendSuccess(); } else { @@ -732,8 +736,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag setStateToNotReadying(); } - private void readyStatus(final String content, final String visibility, final boolean sensitive, - final String spoilerText) { + private void readyStatus(final String visibility, final boolean sensitive) { finishingUploadDialog = ProgressDialog.show( this, getString(R.string.dialog_title_finishing_media_upload), getString(R.string.dialog_message_uploading_media), true, true); @@ -755,9 +758,9 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag finishingUploadDialog.dismiss(); finishingUploadDialog = null; if (successful) { - sendStatus(content, visibility, sensitive, spoilerText); + onReadySuccess(visibility, sensitive); } else { - onReadyFailure(content, visibility, sensitive, spoilerText); + onReadyFailure(visibility, sensitive); } } @@ -780,13 +783,33 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag waitForMediaTask.execute(); } - private void onReadyFailure(final String content, final String visibility, - final boolean sensitive, final String spoilerText) { + private void onReadySuccess(String visibility, boolean sensitive) { + /* Validate the status meets the character limit. This has to be delayed until after all + * uploads finish because their links are added when the upload succeeds and that affects + * whether the limit is met or not. */ + String contentText = textEditor.getText().toString(); + String spoilerText = ""; + if (statusHideText) { + spoilerText = contentWarningEditor.getText().toString(); + } + int characterCount = contentText.length() + spoilerText.length(); + if (characterCount > 0 && characterCount <= STATUS_CHARACTER_LIMIT) { + sendStatus(contentText, visibility, sensitive, spoilerText); + } else if (characterCount <= 0) { + textEditor.setError(getString(R.string.error_empty)); + setStateToNotReadying(); + } else { + textEditor.setError(getString(R.string.error_compose_character_limit)); + setStateToNotReadying(); + } + } + + private void onReadyFailure(final String visibility, final boolean sensitive) { doErrorDialog(R.string.error_media_upload_sending, R.string.action_retry, new View.OnClickListener() { @Override public void onClick(View v) { - readyStatus(content, visibility, sensitive, spoilerText); + readyStatus(visibility, sensitive); } }); setStateToNotReadying(); @@ -951,6 +974,15 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag textEditor.setPadding(textEditor.getPaddingLeft(), textEditor.getPaddingTop(), textEditor.getPaddingRight(), 0); } + // Remove the text URL associated with this media. + if (item.uploadUrl != null) { + Editable text = textEditor.getText(); + int start = text.getSpanStart(item.uploadUrl); + int end = text.getSpanEnd(item.uploadUrl); + if (start != -1 && end != -1) { + text.delete(start, end); + } + } enableMediaButtons(); cancelReadyingMedia(item); } @@ -1052,8 +1084,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag @Override public void onResponse(Call call, retrofit2.Response response) { if (response.isSuccessful()) { - item.id = response.body().id; - waitForMediaLatch.countDown(); + onUploadSuccess(item, response.body()); } else { Log.d(TAG, "Upload request failed. " + response.message()); onUploadFailure(item, call.isCanceled()); @@ -1068,6 +1099,22 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag }); } + private void onUploadSuccess(final QueuedMedia item, Media media) { + item.id = media.id; + + /* Add the upload URL to the text field. Also, keep a reference to the span so if the user + * chooses to remove the media, the URL is also automatically removed. */ + item.uploadUrl = new URLSpan(media.textUrl); + int end = 1 + media.textUrl.length(); + SpannableStringBuilder builder = new SpannableStringBuilder(); + builder.append(' '); + builder.append(media.textUrl); + builder.setSpan(item.uploadUrl, 0, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + textEditor.append(builder); + + waitForMediaLatch.countDown(); + } + private void onUploadFailure(QueuedMedia item, boolean isCanceled) { if (!isCanceled) { /* if the upload was voluntarily cancelled, such as if the user clicked on it to remove diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java index ddb53808..03d9811d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java @@ -36,7 +36,6 @@ import android.util.Base64; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; @@ -44,6 +43,8 @@ import android.widget.ProgressBar; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Profile; +import com.keylesspalace.tusky.util.IOUtils; +import com.keylesspalace.tusky.util.Log; import com.pkmmte.view.CircularImageView; import com.squareup.picasso.Picasso; import com.theartofdev.edmodo.cropper.CropImage; @@ -178,7 +179,7 @@ public class EditProfileActivity extends BaseActivity { .into(avatar); Picasso.with(header.getContext()) .load(me.header) - .placeholder(R.drawable.account_header_missing) + .placeholder(R.drawable.account_header_default) .into(header); } diff --git a/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java b/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java index f45ab2c6..7755f217 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java @@ -23,6 +23,10 @@ import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.view.MenuItem; +import com.keylesspalace.tusky.fragment.SFragment; +import com.keylesspalace.tusky.fragment.TimelineFragment; +import com.keylesspalace.tusky.interfaces.StatusRemoveListener; + public class FavouritesActivity extends BaseActivity implements SFragment.OnUserRemovedListener { private StatusRemoveListener statusRemoveListener; diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java index b85f1faa..9226dc6c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java @@ -32,10 +32,15 @@ import android.text.method.LinkMovementMethod; import android.view.View; import android.widget.Button; import android.widget.EditText; +import android.widget.LinearLayout; import android.widget.TextView; import com.keylesspalace.tusky.entity.AccessToken; import com.keylesspalace.tusky.entity.AppCredentials; +import com.keylesspalace.tusky.network.MastodonAPI; +import com.keylesspalace.tusky.util.CustomTabsHelper; +import com.keylesspalace.tusky.util.Log; +import com.keylesspalace.tusky.util.OkHttpUtils; import java.util.HashMap; import java.util.Map; @@ -58,6 +63,9 @@ public class LoginActivity extends AppCompatActivity { private String clientId; private String clientSecret; + @BindView(R.id.login_input) LinearLayout input; + @BindView(R.id.login_loading) LinearLayout loading; + @BindView(R.id.edit_text_domain) EditText editText; @BindView(R.id.button_login) Button button; @BindView(R.id.whats_an_instance) TextView whatsAnInstance; @@ -322,6 +330,8 @@ public class LoginActivity extends AppCompatActivity { domain = preferences.getString("domain", null); clientId = preferences.getString("clientId", null); clientSecret = preferences.getString("clientSecret", null); + + setLoading(true); /* Since authorization has succeeded, the final step to log in is to exchange * the authorization code for an access token. */ Callback callback = new Callback() { @@ -330,6 +340,8 @@ public class LoginActivity extends AppCompatActivity { if (response.isSuccessful()) { onLoginSuccess(response.body().accessToken); } else { + setLoading(false); + editText.setError(getString(R.string.error_retrieving_oauth_token)); Log.e(TAG, String.format("%s %s", getString(R.string.error_retrieving_oauth_token), @@ -339,6 +351,7 @@ public class LoginActivity extends AppCompatActivity { @Override public void onFailure(Call call, Throwable t) { + setLoading(false); editText.setError(getString(R.string.error_retrieving_oauth_token)); Log.e(TAG, String.format("%s %s", getString(R.string.error_retrieving_oauth_token), @@ -351,21 +364,34 @@ public class LoginActivity extends AppCompatActivity { } else if (error != null) { /* Authorization failed. Put the error response where the user can read it and they * can try again. */ + setLoading(false); editText.setError(getString(R.string.error_authorization_denied)); Log.e(TAG, getString(R.string.error_authorization_denied) + error); } else { + setLoading(false); // This case means a junk response was received somehow. editText.setError(getString(R.string.error_authorization_unknown)); } } } + private void setLoading(boolean loadingState) { + if (loadingState) { + loading.setVisibility(View.VISIBLE); + input.setVisibility(View.GONE); + } else { + loading.setVisibility(View.GONE); + input.setVisibility(View.VISIBLE); + } + } + private void onLoginSuccess(String accessToken) { boolean committed = preferences.edit() .putString("domain", domain) .putString("accessToken", accessToken) .commit(); if (!committed) { + setLoading(false); editText.setError(getString(R.string.error_retrieving_oauth_token)); return; } diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index 243fce57..f0a93c23 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -15,9 +15,10 @@ package com.keylesspalace.tusky; -import android.app.NotificationManager; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.res.Configuration; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -26,9 +27,11 @@ import android.os.PersistableBundle; import android.support.annotation.NonNull; 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.view.ViewPager; +import android.support.v7.app.AlertDialog; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; @@ -41,6 +44,11 @@ import com.arlib.floatingsearchview.FloatingSearchView; import com.arlib.floatingsearchview.suggestions.SearchSuggestionsAdapter; import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion; import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.fragment.SFragment; +import com.keylesspalace.tusky.interfaces.StatusRemoveListener; +import com.keylesspalace.tusky.pager.TimelinePagerAdapter; +import com.keylesspalace.tusky.util.Log; +import com.keylesspalace.tusky.util.ThemeUtils; import com.mikepenz.google_material_typeface_library.GoogleMaterial; import com.mikepenz.materialdrawer.AccountHeader; import com.mikepenz.materialdrawer.AccountHeaderBuilder; @@ -81,7 +89,7 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove @BindView(R.id.tab_layout) TabLayout tabLayout; @BindView(R.id.pager) ViewPager viewPager; - FloatingActionButton composeButton; + public FloatingActionButton composeButton; @Override protected void onCreate(Bundle savedInstanceState) { @@ -206,8 +214,7 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove .putString("current", "[]") .apply(); - ((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))) - .cancel(MessagingService.NOTIFY_ID); + pushNotificationClient.clearNotifications(this); /* After editing a profile, the profile header in the navigation drawer needs to be * refreshed */ @@ -273,7 +280,8 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove } }); - Drawable muteDrawable = ContextCompat.getDrawable(this, R.drawable.ic_mute_24dp); + VectorDrawableCompat muteDrawable = VectorDrawableCompat.create(getResources(), + R.drawable.ic_mute_24dp, getTheme()); ThemeUtils.setDrawableTint(this, muteDrawable, R.attr.toolbar_icon_tint); drawer = new DrawerBuilder() @@ -289,7 +297,8 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove new PrimaryDrawerItem().withIdentifier(3).withName(getString(R.string.action_view_blocks)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_block), new DividerDrawerItem(), new SecondaryDrawerItem().withIdentifier(4).withName(getString(R.string.action_view_preferences)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_settings), - new SecondaryDrawerItem().withIdentifier(5).withName(getString(R.string.action_logout)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_exit_to_app) + new SecondaryDrawerItem().withIdentifier(5).withName(getString(R.string.about_title_activity)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_info), + new SecondaryDrawerItem().withIdentifier(6).withName(getString(R.string.action_logout)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_exit_to_app) ) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override @@ -315,8 +324,11 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove Intent intent = new Intent(MainActivity.this, PreferencesActivity.class); startActivity(intent); } else if (drawerItemIdentifier == 5) { - logout(); + Intent intent = new Intent(MainActivity.this, AboutActivity.class); + startActivity(intent); } else if (drawerItemIdentifier == 6) { + logout(); + } else if (drawerItemIdentifier == 7) { Intent intent = new Intent(MainActivity.this, AccountListActivity.class); intent.putExtra("type", AccountListActivity.Type.FOLLOW_REQUESTS); startActivity(intent); @@ -330,16 +342,27 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove } private void logout() { - if (arePushNotificationsEnabled()) disablePushNotifications(); - - getPrivatePreferences().edit() - .remove("domain") - .remove("accessToken") - .apply(); - - Intent intent = new Intent(MainActivity.this, LoginActivity.class); - startActivity(intent); - finish(); + new AlertDialog.Builder(this) + .setTitle(R.string.action_logout) + .setMessage(R.string.action_logout_confirm) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (arePushNotificationsEnabled()) disablePushNotifications(); + + getPrivatePreferences().edit() + .remove("domain") + .remove("accessToken") + .remove("appAccountId") + .apply(); + + Intent intent = new Intent(MainActivity.this, LoginActivity.class); + startActivity(intent); + finish(); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); } private void setupSearchView() { @@ -472,9 +495,11 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove backgroundHeight = background.getMeasuredHeight(); } + background.setBackgroundColor(ContextCompat.getColor(this, R.color.window_background_dark)); + Picasso.with(MainActivity.this) .load(me.header) - .placeholder(R.drawable.account_header_missing) + .placeholder(R.drawable.account_header_default) .resize(backgroundWidth, backgroundHeight) .centerCrop() .into(background); @@ -553,4 +578,10 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove } } } + + // Fix for GitHub issues #190, #259 (MainActivity won't restart on screen rotation.) + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java b/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java index 53e4f22a..58c18588 100644 --- a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java @@ -21,6 +21,8 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.Nullable; +import com.keylesspalace.tusky.fragment.PreferencesFragment; + public class PreferencesActivity extends BaseActivity implements SharedPreferences.OnSharedPreferenceChangeListener { private boolean themeSwitched; diff --git a/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java b/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java index 0c3d1895..f8e4e447 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java @@ -30,7 +30,11 @@ import android.view.MenuItem; import android.view.View; import android.widget.EditText; +import com.keylesspalace.tusky.adapter.ReportAdapter; import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.util.HtmlUtils; +import com.keylesspalace.tusky.util.Log; +import com.keylesspalace.tusky.util.ThemeUtils; import java.util.ArrayList; import java.util.Arrays; diff --git a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.java b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.java index 96c76617..9e1b732f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.java @@ -19,28 +19,13 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceManager; import android.support.v7.app.AppCompatActivity; -import android.view.Window; -import android.view.WindowManager; public class SplashActivity extends AppCompatActivity { - private static final int SPLASH_TIME_OUT = 2000; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) { - setTheme(R.style.AppTheme_Light); - } - - requestWindowFeature(Window.FEATURE_NO_TITLE); - getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); - - setContentView(R.layout.activity_splash); - /* Determine whether the user is currently logged in, and if so go ahead and load the * timeline. Otherwise, start the activity_login screen. */ SharedPreferences preferences = getSharedPreferences( @@ -48,20 +33,13 @@ public class SplashActivity extends AppCompatActivity { String domain = preferences.getString("domain", null); String accessToken = preferences.getString("accessToken", null); - final Intent intent; - + Intent intent; if (domain != null && accessToken != null) { intent = new Intent(this, MainActivity.class); } else { intent = new Intent(this, LoginActivity.class); } - - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - startActivity(intent); - finish(); - } - }, SPLASH_TIME_OUT); + startActivity(intent); + finish(); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java index 9bce54a1..e73d490a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java @@ -23,6 +23,10 @@ import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.view.MenuItem; +import com.keylesspalace.tusky.fragment.SFragment; +import com.keylesspalace.tusky.fragment.TimelineFragment; +import com.keylesspalace.tusky.interfaces.StatusRemoveListener; + import butterknife.BindView; import butterknife.ButterKnife; diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java index 9dcd5efa..20b3534a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java @@ -15,6 +15,7 @@ package com.keylesspalace.tusky; +import android.content.res.Configuration; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; @@ -24,6 +25,10 @@ import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; +import com.keylesspalace.tusky.fragment.SFragment; +import com.keylesspalace.tusky.fragment.ViewThreadFragment; +import com.keylesspalace.tusky.interfaces.StatusRemoveListener; + public class ViewThreadActivity extends BaseActivity implements SFragment.OnUserRemovedListener { Fragment viewThreadFragment; @@ -74,4 +79,11 @@ public class ViewThreadActivity extends BaseActivity implements SFragment.OnUser listener.removePostsByUser(accountId); } } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + /* Provide a stub to ignore configuration changes so the thread isn't reloaded when the + * the activity is reoriented or resized. */ + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.java index 05ec430e..9f1c4bf0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.java @@ -20,13 +20,16 @@ import android.os.Bundle; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.view.MenuItem; +import android.view.View; import android.widget.MediaController; +import android.widget.ProgressBar; import android.widget.VideoView; import butterknife.BindView; import butterknife.ButterKnife; public class ViewVideoActivity extends BaseActivity { + @BindView(R.id.video_progress) ProgressBar progressBar; @BindView(R.id.video_player) VideoView videoView; @BindView(R.id.toolbar) Toolbar toolbar; @@ -56,6 +59,7 @@ public class ViewVideoActivity extends BaseActivity { videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { + progressBar.setVisibility(View.GONE); mp.setLooping(true); } }); diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java similarity index 87% rename from app/src/main/java/com/keylesspalace/tusky/AccountAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java index 9941d34d..48c6634b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java @@ -13,17 +13,18 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.adapter; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.interfaces.AccountActionListener; import java.util.ArrayList; import java.util.List; -abstract class AccountAdapter extends RecyclerView.Adapter { +public abstract class AccountAdapter extends RecyclerView.Adapter { List accountList; AccountActionListener accountActionListener; @@ -38,7 +39,7 @@ abstract class AccountAdapter extends RecyclerView.Adapter { return accountList.size() + 1; } - void update(List newAccounts) { + public void update(List newAccounts) { if (newAccounts == null || newAccounts.isEmpty()) { return; } @@ -59,14 +60,14 @@ abstract class AccountAdapter extends RecyclerView.Adapter { notifyDataSetChanged(); } - void addItems(List newAccounts) { + public void addItems(List newAccounts) { int end = accountList.size(); accountList.addAll(newAccounts); notifyItemRangeInserted(end, newAccounts.size()); } @Nullable - Account removeItem(int position) { + public Account removeItem(int position) { if (position < 0 || position >= accountList.size()) { return null; } @@ -75,7 +76,7 @@ abstract class AccountAdapter extends RecyclerView.Adapter { return account; } - void addItem(Account account, int position) { + public void addItem(Account account, int position) { if (position < 0 || position > accountList.size()) { return; } diff --git a/app/src/main/java/com/keylesspalace/tusky/BlocksAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java similarity index 94% rename from app/src/main/java/com/keylesspalace/tusky/BlocksAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java index d052db34..e4e34fa9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BlocksAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.adapter; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -22,18 +22,20 @@ import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.TextView; +import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.pkmmte.view.CircularImageView; import com.squareup.picasso.Picasso; import butterknife.BindView; import butterknife.ButterKnife; -class BlocksAdapter extends AccountAdapter { +public class BlocksAdapter extends AccountAdapter { private static final int VIEW_TYPE_BLOCKED_USER = 0; private static final int VIEW_TYPE_FOOTER = 1; - BlocksAdapter(AccountActionListener accountActionListener) { + public BlocksAdapter(AccountActionListener accountActionListener) { super(accountActionListener); } diff --git a/app/src/main/java/com/keylesspalace/tusky/FollowAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java similarity index 93% rename from app/src/main/java/com/keylesspalace/tusky/FollowAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java index 3b09de24..7e9b825f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FollowAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.adapter; import android.content.Context; import android.support.v7.widget.RecyclerView; @@ -22,16 +22,18 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.pkmmte.view.CircularImageView; import com.squareup.picasso.Picasso; /** Both for follows and following lists. */ -class FollowAdapter extends AccountAdapter { +public class FollowAdapter extends AccountAdapter { private static final int VIEW_TYPE_ACCOUNT = 0; private static final int VIEW_TYPE_FOOTER = 1; - FollowAdapter(AccountActionListener accountActionListener) { + public FollowAdapter(AccountActionListener accountActionListener) { super(accountActionListener); } diff --git a/app/src/main/java/com/keylesspalace/tusky/FollowRequestsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java similarity index 94% rename from app/src/main/java/com/keylesspalace/tusky/FollowRequestsAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java index 0a54b558..392d33ac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FollowRequestsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.adapter; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -22,6 +22,8 @@ import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.TextView; +import com.keylesspalace.tusky.interfaces.AccountActionListener; +import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Account; import com.pkmmte.view.CircularImageView; import com.squareup.picasso.Picasso; @@ -29,11 +31,11 @@ import com.squareup.picasso.Picasso; import butterknife.BindView; import butterknife.ButterKnife; -class FollowRequestsAdapter extends AccountAdapter { +public class FollowRequestsAdapter extends AccountAdapter { private static final int VIEW_TYPE_FOLLOW_REQUEST = 0; private static final int VIEW_TYPE_FOOTER = 1; - FollowRequestsAdapter(AccountActionListener accountActionListener) { + public FollowRequestsAdapter(AccountActionListener accountActionListener) { super(accountActionListener); } diff --git a/app/src/main/java/com/keylesspalace/tusky/FooterViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FooterViewHolder.java similarity index 93% rename from app/src/main/java/com/keylesspalace/tusky/FooterViewHolder.java rename to app/src/main/java/com/keylesspalace/tusky/adapter/FooterViewHolder.java index e39ca44a..5ff1187e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FooterViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FooterViewHolder.java @@ -13,12 +13,14 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.adapter; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.ProgressBar; +import com.keylesspalace.tusky.R; + class FooterViewHolder extends RecyclerView.ViewHolder { FooterViewHolder(View itemView) { super(itemView); diff --git a/app/src/main/java/com/keylesspalace/tusky/MutesAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java similarity index 93% rename from app/src/main/java/com/keylesspalace/tusky/MutesAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java index 2513cac6..06258a4b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MutesAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java @@ -1,4 +1,4 @@ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.adapter; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -7,21 +7,20 @@ import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.TextView; +import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.pkmmte.view.CircularImageView; import com.squareup.picasso.Picasso; -import java.util.HashSet; -import java.util.Set; - import butterknife.BindView; import butterknife.ButterKnife; -class MutesAdapter extends AccountAdapter { +public class MutesAdapter extends AccountAdapter { private static final int VIEW_TYPE_MUTED_USER = 0; private static final int VIEW_TYPE_FOOTER = 1; - MutesAdapter(AccountActionListener accountActionListener) { + public MutesAdapter(AccountActionListener accountActionListener) { super(accountActionListener); } diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java similarity index 95% rename from app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 2376469b..639bfea3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.adapter; import android.content.Context; import android.graphics.Typeface; @@ -29,21 +29,24 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.interfaces.AdapterItemRemover; +import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.squareup.picasso.Picasso; import java.util.ArrayList; import java.util.Iterator; import java.util.List; -class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRemover { +public class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRemover { private static final int VIEW_TYPE_MENTION = 0; private static final int VIEW_TYPE_FOOTER = 1; private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2; private static final int VIEW_TYPE_FOLLOW = 3; - enum FooterState { + public enum FooterState { EMPTY, END, LOADING @@ -54,7 +57,7 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe private NotificationActionListener notificationActionListener; private FooterState footerState = FooterState.END; - NotificationsAdapter(StatusActionListener statusListener, + public NotificationsAdapter(StatusActionListener statusListener, NotificationActionListener notificationActionListener) { super(); notifications = new ArrayList<>(); @@ -63,7 +66,7 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe } - void setFooterState(FooterState newFooterState) { + public void setFooterState(FooterState newFooterState) { FooterState oldValue = footerState; footerState = newFooterState; if (footerState != oldValue) { @@ -179,7 +182,7 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe return null; } - void update(List newNotifications) { + public void update(List newNotifications) { if (newNotifications == null || newNotifications.isEmpty()) { return; } @@ -200,7 +203,7 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe notifyDataSetChanged(); } - void addItems(List new_notifications) { + public void addItems(List new_notifications) { int end = notifications.size(); notifications.addAll(new_notifications); notifyItemRangeInserted(end, new_notifications.size()); @@ -223,7 +226,7 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe } } - interface NotificationActionListener { + public interface NotificationActionListener { void onViewAccount(String id); } diff --git a/app/src/main/java/com/keylesspalace/tusky/ReportAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/ReportAdapter.java similarity index 90% rename from app/src/main/java/com/keylesspalace/tusky/ReportAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/adapter/ReportAdapter.java index ccdf7aa9..50e07369 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ReportAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ReportAdapter.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.adapter; import android.support.v7.widget.RecyclerView; import android.text.Spanned; @@ -24,16 +24,18 @@ import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.TextView; +import com.keylesspalace.tusky.R; + import java.util.ArrayList; import java.util.List; -class ReportAdapter extends RecyclerView.Adapter { - static class ReportStatus { +public class ReportAdapter extends RecyclerView.Adapter { + public static class ReportStatus { String id; Spanned content; boolean checked; - ReportStatus(String id, Spanned content, boolean checked) { + public ReportStatus(String id, Spanned content, boolean checked) { this.id = id; this.content = content; this.checked = checked; @@ -58,7 +60,7 @@ class ReportAdapter extends RecyclerView.Adapter { private List statusList; - ReportAdapter() { + public ReportAdapter() { super(); statusList = new ArrayList<>(); } @@ -82,13 +84,13 @@ class ReportAdapter extends RecyclerView.Adapter { return statusList.size(); } - void addItem(ReportStatus status) { + public void addItem(ReportStatus status) { int end = statusList.size(); statusList.add(status); notifyItemInserted(end); } - void addItems(List newStatuses) { + public void addItems(List newStatuses) { int end = statusList.size(); int added = 0; for (ReportStatus status : newStatuses) { @@ -102,7 +104,7 @@ class ReportAdapter extends RecyclerView.Adapter { } } - String[] getCheckedStatusIds() { + public String[] getCheckedStatusIds() { List idList = new ArrayList<>(); for (ReportStatus status : statusList) { if (status.checked) { diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java similarity index 89% rename from app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java rename to app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 6a2e431d..1d029e86 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -13,9 +13,10 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.adapter; import android.content.Context; +import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.text.Spanned; @@ -26,7 +27,13 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.ToggleButton; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.util.RoundedTransformation; +import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.util.DateUtils; +import com.keylesspalace.tusky.util.LinkHelper; +import com.keylesspalace.tusky.util.ThemeUtils; import com.squareup.picasso.Picasso; import com.varunest.sparkbutton.SparkButton; import com.varunest.sparkbutton.SparkEventListener; @@ -100,7 +107,10 @@ class StatusViewHolder extends RecyclerView.ViewHolder { StatusActionListener listener) { /* Redirect URLSpan's in the status content to the listener for viewing tag pages and * account pages. */ - LinkHelper.setClickableText(this.content, content, mentions, listener); + Context context = this.content.getContext(); + boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean("useCustomTabs", true); + LinkHelper.setClickableText(this.content, content, mentions, useCustomTabs, listener); } private void setAvatar(String url) { @@ -290,14 +300,20 @@ class StatusViewHolder extends RecyclerView.ViewHolder { } }); reblogButton.setEventListener(new SparkEventListener() { - @Override - public void onEvent(ImageView button, boolean buttonState) { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onReblog(!reblogged, position); - } - } - }); + @Override + public void onEvent(ImageView button, boolean buttonState) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onReblog(!reblogged, position); + } + } + + @Override + public void onEventAnimationEnd(ImageView button, boolean buttonState) {} + + @Override + public void onEventAnimationStart(ImageView button, boolean buttonState) {} + }); favouriteButton.setEventListener(new SparkEventListener() { @Override public void onEvent(ImageView button, boolean buttonState) { @@ -306,6 +322,12 @@ class StatusViewHolder extends RecyclerView.ViewHolder { listener.onFavourite(!favourited, position); } } + + @Override + public void onEventAnimationEnd(ImageView button, boolean buttonState) {} + + @Override + public void onEventAnimationStart(ImageView button, boolean buttonState) {} }); moreButton.setOnClickListener(new View.OnClickListener() { @Override diff --git a/app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java similarity index 85% rename from app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java index afa407f8..46523f52 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java @@ -13,25 +13,27 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.adapter; -import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.interfaces.AdapterItemRemover; +import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.entity.Status; import java.util.ArrayList; import java.util.List; -class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRemover { +public class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRemover { private List statuses; private StatusActionListener statusActionListener; private int statusIndex; - ThreadAdapter(StatusActionListener listener) { + public ThreadAdapter(StatusActionListener listener) { this.statusActionListener = listener; this.statuses = new ArrayList<>(); this.statusIndex = 0; @@ -56,7 +58,7 @@ class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRemover { return statuses.size(); } - Status getItem(int position) { + public Status getItem(int position) { return statuses.get(position); } @@ -77,7 +79,7 @@ class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRemover { } } - int setStatus(Status status) { + public int setStatus(Status status) { if (statuses.size() > 0 && statuses.get(statusIndex).equals(status)) { // Do not add this status on refresh, it's already in there. statuses.set(statusIndex, status); @@ -89,16 +91,16 @@ class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRemover { return i; } - void setContext(List ancestors, List descendants) { + public void setContext(List ancestors, List descendants) { Status mainStatus = null; // In case of refresh, remove old ancestors and descendants first. We'll remove all blindly, // as we have no guarantee on their order to be the same as before - int old_size = statuses.size(); - if (old_size > 0) { + int oldSize = statuses.size(); + if (oldSize > 0) { mainStatus = statuses.get(statusIndex); statuses.clear(); - notifyItemRangeRemoved(0, old_size); + notifyItemRangeRemoved(0, oldSize); } // Insert newly fetched ancestors diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java similarity index 86% rename from app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java index 759a2357..4f06ef47 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.adapter; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; @@ -21,16 +21,19 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.interfaces.AdapterItemRemover; +import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.entity.Status; import java.util.ArrayList; import java.util.List; -class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover { +public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover { private static final int VIEW_TYPE_STATUS = 0; private static final int VIEW_TYPE_FOOTER = 1; - enum FooterState { + public enum FooterState { EMPTY, END, LOADING @@ -40,7 +43,7 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover private StatusActionListener statusListener; private FooterState footerState = FooterState.END; - TimelineAdapter(StatusActionListener statusListener) { + public TimelineAdapter(StatusActionListener statusListener) { super(); statuses = new ArrayList<>(); this.statusListener = statusListener; @@ -79,7 +82,7 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover } } - void setFooterState(FooterState newFooterState) { + public void setFooterState(FooterState newFooterState) { FooterState oldValue = footerState; footerState = newFooterState; if (footerState != oldValue) { @@ -110,7 +113,7 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover } } - void update(List newStatuses) { + public void update(List newStatuses) { if (newStatuses == null || newStatuses.isEmpty()) { return; } @@ -131,7 +134,7 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover notifyDataSetChanged(); } - void addItems(List newStatuses) { + public void addItems(List newStatuses) { int end = statuses.size(); statuses.addAll(newStatuses); notifyItemRangeInserted(end, newStatuses.size()); @@ -142,7 +145,12 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover notifyItemRemoved(position); } - void removeAllByAccountId(String accountId) { + public void clear() { + statuses.clear(); + notifyDataSetChanged(); + } + + public void removeAllByAccountId(String accountId) { for (int i = 0; i < statuses.size();) { Status status = statuses.get(i); if (accountId.equals(status.account.id)) { @@ -155,7 +163,7 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover } @Nullable - Status getItem(int position) { + public Status getItem(int position) { if (position >= 0 && position < statuses.size()) { return statuses.get(position); } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.java b/app/src/main/java/com/keylesspalace/tusky/entity/Account.java index e7be8e81..c802bd4a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Account.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.java @@ -20,8 +20,8 @@ import android.text.Spanned; import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion; import com.google.gson.annotations.SerializedName; -import com.keylesspalace.tusky.HtmlUtils; -import com.keylesspalace.tusky.StringWithEmoji; +import com.keylesspalace.tusky.util.HtmlUtils; +import com.keylesspalace.tusky.json.StringWithEmoji; public class Account implements SearchSuggestion { public String id; diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Session.java b/app/src/main/java/com/keylesspalace/tusky/entity/Session.java new file mode 100644 index 00000000..8a3c5d0b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Session.java @@ -0,0 +1,28 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity; + +public class Session { + public String instanceUrl; + public String accessToken; + public String deviceToken; + + public Session(String instanceUrl, String accessToken, String deviceToken) { + this.instanceUrl = instanceUrl; + this.accessToken = accessToken; + this.deviceToken = deviceToken; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java index 4e147358..c98ebfa5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java @@ -17,6 +17,10 @@ package com.keylesspalace.tusky.entity; import android.text.Spanned; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; import java.util.Date; @@ -115,6 +119,7 @@ public class Status { } public static class MediaAttachment { + @com.google.gson.annotations.JsonAdapter(MediaTypeDeserializer.class) public enum Type { @SerializedName("image") IMAGE, @@ -122,7 +127,7 @@ public class Status { GIFV, @SerializedName("video") VIDEO, - UNKNOWN, + UNKNOWN } public String url; @@ -137,6 +142,23 @@ public class Status { public String remoteUrl; public Type type; + + static class MediaTypeDeserializer implements JsonDeserializer { + @Override + public Type deserialize(JsonElement json, java.lang.reflect.Type classOfT, JsonDeserializationContext context) + throws JsonParseException { + switch(json.toString()) { + case "\"image\"": + return Type.IMAGE; + case "\"gifv\"": + return Type.GIFV; + case "\"video\"": + return Type.VIDEO; + default: + return Type.UNKNOWN; + } + } + } } public static class Mention { @@ -150,4 +172,6 @@ public class Status { @SerializedName("username") public String localUsername; } + + } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountListFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java similarity index 94% rename from app/src/main/java/com/keylesspalace/tusky/AccountListFragment.java rename to app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java index 1128602f..eed0d72a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountListFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.fragment; import android.content.Context; import android.content.Intent; @@ -29,8 +29,22 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; + +import com.keylesspalace.tusky.AccountActivity; +import com.keylesspalace.tusky.adapter.AccountAdapter; +import com.keylesspalace.tusky.adapter.BlocksAdapter; +import com.keylesspalace.tusky.adapter.FollowAdapter; +import com.keylesspalace.tusky.adapter.FollowRequestsAdapter; +import com.keylesspalace.tusky.adapter.MutesAdapter; +import com.keylesspalace.tusky.BaseActivity; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Relationship; +import com.keylesspalace.tusky.interfaces.AccountActionListener; +import com.keylesspalace.tusky.network.MastodonAPI; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.util.EndlessOnScrollListener; +import com.keylesspalace.tusky.util.Log; +import com.keylesspalace.tusky.util.ThemeUtils; import java.util.List; @@ -114,16 +128,22 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi } recyclerView.setAdapter(adapter); + return rootView; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + BaseActivity activity = (BaseActivity) getActivity(); + if (jumpToTopAllowed()) { - TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout); + TabLayout layout = (TabLayout) activity.findViewById(R.id.tab_layout); onTabSelectedListener = new TabLayout.OnTabSelectedListener() { @Override - public void onTabSelected(TabLayout.Tab tab) { - } + public void onTabSelected(TabLayout.Tab tab) {} @Override - public void onTabUnselected(TabLayout.Tab tab) { - } + public void onTabUnselected(TabLayout.Tab tab) {} @Override public void onTabReselected(TabLayout.Tab tab) { @@ -133,16 +153,10 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi layout.addOnTabSelectedListener(onTabSelectedListener); } - return rootView; - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); /* 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 = ((BaseActivity) getActivity()).mastodonAPI; + api = activity.mastodonAPI; scrollListener = new EndlessOnScrollListener(layoutManager) { @Override public void onLoadMore(int page, int totalItemsCount, RecyclerView view) { diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java similarity index 95% rename from app/src/main/java/com/keylesspalace/tusky/BaseFragment.java rename to app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java index 0a9dcbcf..c0305d12 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.fragment; import android.content.Context; import android.content.SharedPreferences; @@ -21,6 +21,8 @@ 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; diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeOptionsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ComposeOptionsFragment.java similarity index 69% rename from app/src/main/java/com/keylesspalace/tusky/ComposeOptionsFragment.java rename to app/src/main/java/com/keylesspalace/tusky/fragment/ComposeOptionsFragment.java index 3f3e1c0b..ab522602 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeOptionsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ComposeOptionsFragment.java @@ -13,12 +13,18 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.fragment; import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Bundle; +import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; import android.support.design.widget.BottomSheetDialogFragment; +import android.support.graphics.drawable.VectorDrawableCompat; +import android.support.v4.graphics.drawable.DrawableCompat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -27,8 +33,11 @@ import android.widget.CompoundButton; import android.widget.RadioButton; import android.widget.RadioGroup; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.util.ThemeUtils; + public class ComposeOptionsFragment extends BottomSheetDialogFragment { - interface Listener { + public interface Listener { void onVisibilityChanged(String visibility); void onContentWarningChanged(boolean hideText); } @@ -85,8 +94,16 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment { } radio.check(radioCheckedId); + RadioButton publicButton = (RadioButton) rootView.findViewById(R.id.radio_public); + RadioButton unlistedButton = (RadioButton) rootView.findViewById(R.id.radio_unlisted); + RadioButton privateButton = (RadioButton) rootView.findViewById(R.id.radio_private); + RadioButton directButton = (RadioButton) rootView.findViewById(R.id.radio_direct); + setRadioButtonDrawable(getContext(), publicButton, R.drawable.ic_public_24dp); + setRadioButtonDrawable(getContext(), unlistedButton, R.drawable.ic_lock_open_24dp); + setRadioButtonDrawable(getContext(), privateButton, R.drawable.ic_lock_outline_24dp); + setRadioButtonDrawable(getContext(), directButton, R.drawable.ic_email_24dp); + if (isReply) { - RadioButton publicButton = (RadioButton) rootView.findViewById(R.id.radio_public); publicButton.setEnabled(false); } @@ -132,4 +149,27 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment { } }); } + + private static void setRadioButtonDrawable(Context context, RadioButton button, + @DrawableRes int id) { + ColorStateList list = new ColorStateList(new int[][] { + new int[] { -android.R.attr.state_checked }, + new int[] { android.R.attr.state_checked } + }, new int[] { + ThemeUtils.getColor(context, R.attr.compose_image_button_tint), + ThemeUtils.getColor(context, R.attr.colorAccent) + }); + Drawable drawable = VectorDrawableCompat.create(context.getResources(), id, + context.getTheme()); + if (drawable == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + button.setButtonTintList(list); + } else { + drawable = DrawableCompat.wrap(drawable); + DrawableCompat.setTintList(drawable, list); + } + button.setButtonDrawable(drawable); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java similarity index 96% rename from app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java rename to app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 56afc0fd..bef04290 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.fragment; import android.content.Context; import android.content.SharedPreferences; @@ -31,8 +31,17 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; + +import com.keylesspalace.tusky.MainActivity; +import com.keylesspalace.tusky.adapter.NotificationsAdapter; +import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.interfaces.StatusRemoveListener; +import com.keylesspalace.tusky.util.EndlessOnScrollListener; +import com.keylesspalace.tusky.util.Log; +import com.keylesspalace.tusky.util.ThemeUtils; import java.util.List; diff --git a/app/src/main/java/com/keylesspalace/tusky/PreferencesFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java similarity index 92% rename from app/src/main/java/com/keylesspalace/tusky/PreferencesFragment.java rename to app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java index 8820f5eb..280098a7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/PreferencesFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java @@ -13,11 +13,13 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.fragment; import android.os.Bundle; import android.preference.PreferenceFragment; +import com.keylesspalace.tusky.R; + public class PreferencesFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/com/keylesspalace/tusky/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java similarity index 95% rename from app/src/main/java/com/keylesspalace/tusky/SFragment.java rename to app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index 3af5cce0..71b04426 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.fragment; import android.content.Intent; import android.content.SharedPreferences; @@ -27,8 +27,19 @@ import android.text.Spanned; import android.view.MenuItem; import android.view.View; +import com.keylesspalace.tusky.AccountActivity; +import com.keylesspalace.tusky.BaseActivity; +import com.keylesspalace.tusky.ComposeActivity; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.ReportActivity; +import com.keylesspalace.tusky.ViewTagActivity; +import com.keylesspalace.tusky.ViewThreadActivity; +import com.keylesspalace.tusky.ViewVideoActivity; 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.util.HtmlUtils; import java.util.ArrayList; import java.util.List; @@ -45,7 +56,7 @@ import retrofit2.Response; * overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear * up what needs to be where. */ public abstract class SFragment extends BaseFragment { - interface OnUserRemovedListener { + public interface OnUserRemovedListener { void onUserRemoved(String accountId); } diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java similarity index 83% rename from app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java rename to app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index d23bbed3..17e9b726 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.fragment; import android.content.Context; import android.content.SharedPreferences; @@ -23,6 +23,7 @@ import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.TabLayout; +import android.support.v4.content.LocalBroadcastManager; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; @@ -31,12 +32,23 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.keylesspalace.tusky.MainActivity; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.adapter.TimelineAdapter; import com.keylesspalace.tusky.entity.Status; - +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.interfaces.StatusRemoveListener; +import com.keylesspalace.tusky.util.EndlessOnScrollListener; +import com.keylesspalace.tusky.util.Log; +import com.keylesspalace.tusky.util.TimelineReceiver; +import com.keylesspalace.tusky.util.ThemeUtils; + +import java.util.Iterator; import java.util.List; import retrofit2.Call; import retrofit2.Callback; +import retrofit2.Response; public class TimelineFragment extends SFragment implements SwipeRefreshLayout.OnRefreshListener, @@ -45,9 +57,7 @@ public class TimelineFragment extends SFragment implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = "Timeline"; // logging tag - private Call> listCall; - - enum Kind { + public enum Kind { HOME, PUBLIC_LOCAL, PUBLIC_FEDERATED, @@ -64,7 +74,11 @@ public class TimelineFragment extends SFragment implements private LinearLayoutManager layoutManager; private EndlessOnScrollListener scrollListener; private TabLayout.OnTabSelectedListener onTabSelectedListener; + private SharedPreferences preferences; + private boolean filterRemoveReplies; + private boolean filterRemoveReblogs; private boolean hideFab; + private TimelineReceiver timelineReceiver; public static TimelineFragment newInstance(Kind kind) { TimelineFragment fragment = new TimelineFragment(); @@ -113,6 +127,25 @@ public class TimelineFragment extends SFragment implements adapter = new TimelineAdapter(this); recyclerView.setAdapter(adapter); + timelineReceiver = new TimelineReceiver(adapter); + LocalBroadcastManager.getInstance(context.getApplicationContext()).registerReceiver(timelineReceiver, TimelineReceiver.getFilter(kind)); + return rootView; + } + + private void onLoadMore(RecyclerView view) { + TimelineAdapter adapter = (TimelineAdapter) view.getAdapter(); + Status status = adapter.getItem(adapter.getItemCount() - 2); + if (status != null) { + sendFetchTimelineRequest(status.id, null); + } else { + sendFetchTimelineRequest(null, null); + } + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (jumpToTopAllowed()) { TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout); onTabSelectedListener = new TabLayout.OnTabSelectedListener() { @@ -130,23 +163,6 @@ public class TimelineFragment extends SFragment implements layout.addOnTabSelectedListener(onTabSelectedListener); } - return rootView; - } - - private void onLoadMore(RecyclerView view) { - TimelineAdapter adapter = (TimelineAdapter) view.getAdapter(); - Status status = adapter.getItem(adapter.getItemCount() - 2); - if (status != null) { - sendFetchTimelineRequest(status.id, null); - } else { - sendFetchTimelineRequest(); - } - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't * guaranteed to be set until then. */ if (composeButtonPresent()) { @@ -189,12 +205,8 @@ public class TimelineFragment extends SFragment implements }; } recyclerView.addOnScrollListener(scrollListener); - } - @Override - public void onDestroy() { - super.onDestroy(); - if (listCall != null) listCall.cancel(); + preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); } @Override @@ -203,6 +215,7 @@ public class TimelineFragment extends SFragment implements TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout); tabLayout.removeOnTabSelectedListener(onTabSelectedListener); } + LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(timelineReceiver); super.onDestroyView(); } @@ -224,9 +237,9 @@ public class TimelineFragment extends SFragment implements adapter.setFooterState(TimelineAdapter.FooterState.LOADING); } - Callback> cb = new Callback>() { + Callback> callback = new Callback>() { @Override - public void onResponse(Call> call, retrofit2.Response> response) { + public void onResponse(Call> call, Response> response) { if (response.isSuccessful()) { onFetchTimelineSuccess(response.body(), fromId); } else { @@ -240,6 +253,7 @@ public class TimelineFragment extends SFragment implements } }; + Call> listCall; switch (kind) { default: case HOME: { @@ -268,11 +282,7 @@ public class TimelineFragment extends SFragment implements } } callList.add(listCall); - listCall.enqueue(cb); - } - - private void sendFetchTimelineRequest() { - sendFetchTimelineRequest(null, null); + listCall.enqueue(callback); } public void removePostsByUser(String accountId) { @@ -288,7 +298,36 @@ public class TimelineFragment extends SFragment implements return false; } + protected void filterStatuses(List statuses) { + Iterator it = statuses.iterator(); + while (it.hasNext()) { + Status status = it.next(); + if ((status.inReplyToId != null && filterRemoveReplies) || (status.reblog != null && filterRemoveReblogs)) { + it.remove(); + } + } + } + + protected void setFiltersFromSettings() { + boolean oldRemoveReplies = filterRemoveReplies; + boolean oldRemoveReblogs = filterRemoveReblogs; + filterRemoveReplies = (kind == Kind.HOME && !preferences.getBoolean("tabFilterHomeReplies", true)); + filterRemoveReblogs = (kind == Kind.HOME && !preferences.getBoolean("tabFilterHomeBoosts", true)); + + if (adapter.getItemCount() > 1 && (oldRemoveReblogs != filterRemoveReblogs || oldRemoveReplies != filterRemoveReplies)) { + adapter.clear(); + sendFetchTimelineRequest(null, null); + } + } + + @Override + public void onResume() { + super.onResume(); + setFiltersFromSettings(); + } + public void onFetchTimelineSuccess(List statuses, String fromId) { + filterStatuses(statuses); if (fromId != null) { if (statuses.size() > 0 && !findStatus(statuses, fromId)) { adapter.addItems(statuses); @@ -314,7 +353,7 @@ public class TimelineFragment extends SFragment implements if (status != null) { sendFetchTimelineRequest(null, status.id); } else { - sendFetchTimelineRequest(); + sendFetchTimelineRequest(null, null); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.java similarity index 75% rename from app/src/main/java/com/keylesspalace/tusky/ViewMediaFragment.java rename to app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.java index fed8cbeb..f53a4ef6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.java @@ -13,12 +13,10 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.fragment; -import android.app.AlertDialog; import android.app.DownloadManager; import android.content.Context; -import android.content.DialogInterface; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; @@ -30,12 +28,20 @@ import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; import android.support.v4.app.DialogFragment; import android.support.v4.content.ContextCompat; +import android.support.v7.widget.Toolbar; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.widget.ImageView; +import com.github.chrisbanes.photoview.OnOutsidePhotoTapListener; +import com.github.chrisbanes.photoview.OnSingleFlingListener; +import com.github.chrisbanes.photoview.PhotoView; +import com.github.chrisbanes.photoview.PhotoViewAttacher; +import com.keylesspalace.tusky.R; import com.squareup.picasso.Callback; import com.squareup.picasso.Picasso; @@ -43,17 +49,18 @@ import java.io.File; import butterknife.BindView; import butterknife.ButterKnife; -import uk.co.senab.photoview.PhotoView; -import uk.co.senab.photoview.PhotoViewAttacher; -public class ViewMediaFragment extends DialogFragment { +public class ViewMediaFragment extends DialogFragment implements Toolbar.OnMenuItemClickListener { private PhotoViewAttacher attacher; - private DownloadManager downloadManager; private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1; - @BindView(R.id.view_media_image) PhotoView photoView; + @BindView(R.id.view_media_image) + PhotoView photoView; + + @BindView(R.id.toolbar) + Toolbar toolbar; public static ViewMediaFragment newInstance(String url) { Bundle arguments = new Bundle(); @@ -81,7 +88,7 @@ public class ViewMediaFragment extends DialogFragment { @Override public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_view_media, container, false); + final View rootView = inflater.inflate(R.layout.fragment_view_media, container, false); ButterKnife.bind(this, rootView); Bundle arguments = getArguments(); @@ -90,24 +97,19 @@ public class ViewMediaFragment extends DialogFragment { attacher = new PhotoViewAttacher(photoView); // Clicking outside the photo closes the viewer. - attacher.setOnPhotoTapListener(new PhotoViewAttacher.OnPhotoTapListener() { - @Override - public void onPhotoTap(View view, float x, float y) { - - } - + attacher.setOnOutsidePhotoTapListener(new OnOutsidePhotoTapListener() { @Override - public void onOutsidePhotoTap() { + public void onOutsidePhotoTap(ImageView imageView) { dismiss(); } }); /* A vertical swipe motion also closes the viewer. This is especially useful when the photo * mostly fills the screen so clicking outside is difficult. */ - attacher.setOnSingleFlingListener(new PhotoViewAttacher.OnSingleFlingListener() { + attacher.setOnSingleFlingListener(new OnSingleFlingListener() { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, - float velocityY) { + float velocityY) { if (Math.abs(velocityY) > Math.abs(velocityX)) { dismiss(); return true; @@ -116,22 +118,10 @@ public class ViewMediaFragment extends DialogFragment { } }); - attacher.setOnLongClickListener(new View.OnLongClickListener() { + toolbar.setNavigationOnClickListener(new View.OnClickListener() { @Override - public boolean onLongClick(View v) { - - AlertDialog downloadDialog = new AlertDialog.Builder(getContext()).create(); - - downloadDialog.setButton(AlertDialog.BUTTON_NEUTRAL, getString(R.string.dialog_download_image), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - - downloadImage(); - } - }); - downloadDialog.show(); - return false; + public void onClick(View v) { + dismiss(); } }); @@ -140,6 +130,10 @@ public class ViewMediaFragment extends DialogFragment { .into(photoView, new Callback() { @Override public void onSuccess() { + rootView.findViewById(R.id.view_media_progress).setVisibility(View.GONE); + toolbar.setOnMenuItemClickListener(ViewMediaFragment.this); + toolbar.inflateMenu(R.menu.view_media_tooblar); + attacher.update(); } @@ -152,12 +146,6 @@ public class ViewMediaFragment extends DialogFragment { return rootView; } - @Override - public void onDestroyView() { - attacher.cleanup(); - super.onDestroyView(); - } - private void downloadImage(){ //Permission stuff @@ -170,14 +158,13 @@ public class ViewMediaFragment extends DialogFragment { PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE); } else { - //download stuff String url = getArguments().getString("url"); Uri uri = Uri.parse(url); String filename = new File(url).getName(); - downloadManager = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE); + DownloadManager downloadManager = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE); DownloadManager.Request request = new DownloadManager.Request(uri); request.allowScanningByMediaScanner(); @@ -211,9 +198,24 @@ public class ViewMediaFragment extends DialogFragment { private void doErrorDialog(@StringRes int descriptionId, @StringRes int actionId, View.OnClickListener listener) { - Snackbar bar = Snackbar.make(getView(), getString(descriptionId), - Snackbar.LENGTH_SHORT); - bar.setAction(actionId, listener); - bar.show(); + if(getView() != null) { + Snackbar bar = Snackbar.make(getView(), getString(descriptionId), + Snackbar.LENGTH_SHORT); + bar.setAction(actionId, listener); + bar.show(); + } + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + int id = item.getItemId(); + switch (id) { + case R.id.action_download: + downloadImage(); + break; + default: + break; + } + return true; } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java similarity index 81% rename from app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java rename to app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index 0cf47600..d02f4359 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.fragment; import android.content.Context; import android.graphics.drawable.Drawable; @@ -29,11 +29,22 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; + +import com.keylesspalace.tusky.adapter.ThreadAdapter; +import com.keylesspalace.tusky.BaseActivity; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.StatusContext; +import com.keylesspalace.tusky.network.MastodonAPI; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.interfaces.StatusRemoveListener; +import com.keylesspalace.tusky.util.ConversationLineItemDecoration; +import com.keylesspalace.tusky.util.Log; +import com.keylesspalace.tusky.util.ThemeUtils; import retrofit2.Call; import retrofit2.Callback; +import retrofit2.Response; public class ViewThreadFragment extends SFragment implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener, StatusRemoveListener { @@ -42,6 +53,7 @@ public class ViewThreadFragment extends SFragment implements private SwipeRefreshLayout swipeRefreshLayout; private RecyclerView recyclerView; private ThreadAdapter adapter; + private MastodonAPI mastodonApi; private String thisThreadsStatusId; public static ViewThreadFragment newInstance(String id) { @@ -72,25 +84,34 @@ public class ViewThreadFragment extends SFragment implements R.drawable.status_divider_dark); divider.setDrawable(drawable); recyclerView.addItemDecoration(divider); - recyclerView.addItemDecoration(new ConversationLineItemDecoration(context, ContextCompat.getDrawable(context, R.drawable.conversation_divider_dark))); + recyclerView.addItemDecoration(new ConversationLineItemDecoration(context, + ContextCompat.getDrawable(context, R.drawable.conversation_divider_dark))); adapter = new ThreadAdapter(this); recyclerView.setAdapter(adapter); - String id = getArguments().getString("id"); - sendStatusRequest(id); - sendThreadRequest(id); - thisThreadsStatusId = id; + mastodonApi = null; + thisThreadsStatusId = null; return rootView; } - private void sendStatusRequest(final String id) { - MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI; + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); - Call call = api.status(id); + /* BaseActivity's MastodonAPI object isn't guaranteed to be valid until after its onCreate + * is run, so all calls that need it can't be done until here. */ + mastodonApi = ((BaseActivity) getActivity()).mastodonAPI; + + thisThreadsStatusId = getArguments().getString("id"); + onRefresh(); + } + + private void sendStatusRequest(final String id) { + Call call = mastodonApi.status(id); call.enqueue(new Callback() { @Override - public void onResponse(Call call, retrofit2.Response response) { + public void onResponse(Call call, Response response) { if (response.isSuccessful()) { int position = adapter.setStatus(response.body()); recyclerView.scrollToPosition(position); @@ -108,12 +129,10 @@ public class ViewThreadFragment extends SFragment implements } private void sendThreadRequest(final String id) { - MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI; - - Call call = api.statusContext(id); + Call call = mastodonApi.statusContext(id); call.enqueue(new Callback() { @Override - public void onResponse(Call call, retrofit2.Response response) { + public void onResponse(Call call, Response response) { if (response.isSuccessful()) { swipeRefreshLayout.setRefreshing(false); StatusContext context = response.body(); diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.java similarity index 92% rename from app/src/main/java/com/keylesspalace/tusky/AccountActionListener.java rename to app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.java index bca609cd..116bcae8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.java @@ -13,9 +13,9 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.interfaces; -interface AccountActionListener { +public interface AccountActionListener { void onViewAccount(String id); void onMute(final boolean mute, final String id, final int position); void onBlock(final boolean block, final String id, final int position); diff --git a/app/src/main/java/com/keylesspalace/tusky/AdapterItemRemover.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/AdapterItemRemover.java similarity index 89% rename from app/src/main/java/com/keylesspalace/tusky/AdapterItemRemover.java rename to app/src/main/java/com/keylesspalace/tusky/interfaces/AdapterItemRemover.java index 634935ff..5b49cbfa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AdapterItemRemover.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/AdapterItemRemover.java @@ -13,8 +13,8 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.interfaces; -interface AdapterItemRemover { +public interface AdapterItemRemover { void removeItem(int position); } diff --git a/app/src/main/java/com/keylesspalace/tusky/LinkListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java similarity index 90% rename from app/src/main/java/com/keylesspalace/tusky/LinkListener.java rename to app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java index 9e188ae4..62360e34 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LinkListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java @@ -13,9 +13,9 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.interfaces; -interface LinkListener { +public interface LinkListener { void onViewTag(String tag); void onViewAccount(String id); } diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java similarity index 91% rename from app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java rename to app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java index 1004bb82..b6b80f72 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -13,13 +13,13 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.interfaces; import android.view.View; import com.keylesspalace.tusky.entity.Status; -interface StatusActionListener extends LinkListener { +public interface StatusActionListener extends LinkListener { void onReply(int position); void onReblog(final boolean reblog, final int position); void onFavourite(final boolean favourite, final int position); diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusRemoveListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusRemoveListener.java similarity index 89% rename from app/src/main/java/com/keylesspalace/tusky/StatusRemoveListener.java rename to app/src/main/java/com/keylesspalace/tusky/interfaces/StatusRemoveListener.java index 23111c6a..c05f7657 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusRemoveListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusRemoveListener.java @@ -13,8 +13,8 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.interfaces; -interface StatusRemoveListener { +public interface StatusRemoveListener { void removePostsByUser(String accountId); } diff --git a/app/src/main/java/com/keylesspalace/tusky/SpannedTypeAdapter.java b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.java similarity index 93% rename from app/src/main/java/com/keylesspalace/tusky/SpannedTypeAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.java index 3b47acc3..305adba4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SpannedTypeAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.json; import android.text.Spanned; @@ -22,6 +22,7 @@ import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; +import com.keylesspalace.tusky.util.HtmlUtils; import java.lang.reflect.Type; diff --git a/app/src/main/java/com/keylesspalace/tusky/StringWithEmoji.java b/app/src/main/java/com/keylesspalace/tusky/json/StringWithEmoji.java similarity index 96% rename from app/src/main/java/com/keylesspalace/tusky/StringWithEmoji.java rename to app/src/main/java/com/keylesspalace/tusky/json/StringWithEmoji.java index cad8e8c0..d229c5a9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StringWithEmoji.java +++ b/app/src/main/java/com/keylesspalace/tusky/json/StringWithEmoji.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.json; /** * This is just a wrapper class for a String. diff --git a/app/src/main/java/com/keylesspalace/tusky/StringWithEmojiTypeAdapter.java b/app/src/main/java/com/keylesspalace/tusky/json/StringWithEmojiTypeAdapter.java similarity index 91% rename from app/src/main/java/com/keylesspalace/tusky/StringWithEmojiTypeAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/json/StringWithEmojiTypeAdapter.java index ecf18bcd..86bd2c61 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StringWithEmojiTypeAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/json/StringWithEmojiTypeAdapter.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.json; import com.emojione.Emojione; import com.google.gson.JsonDeserializationContext; @@ -24,7 +24,7 @@ import com.google.gson.JsonParseException; import java.lang.reflect.Type; /** This is a type-based workaround to allow for shortcode conversion when loading display names. */ -class StringWithEmojiTypeAdapter implements JsonDeserializer { +public class StringWithEmojiTypeAdapter implements JsonDeserializer { @Override public StringWithEmoji deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { diff --git a/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonAPI.java similarity index 99% rename from app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java rename to app/src/main/java/com/keylesspalace/tusky/network/MastodonAPI.java index 011fbc40..aad04523 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonAPI.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.network; import com.keylesspalace.tusky.entity.AccessToken; import com.keylesspalace.tusky.entity.Account; diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyAPI.java b/app/src/main/java/com/keylesspalace/tusky/network/TuskyApi.java similarity index 64% rename from app/src/main/java/com/keylesspalace/tusky/TuskyAPI.java rename to app/src/main/java/com/keylesspalace/tusky/network/TuskyApi.java index 4831949b..24384dcf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyAPI.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/TuskyApi.java @@ -13,19 +13,18 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.network; + +import com.keylesspalace.tusky.entity.Session; import okhttp3.ResponseBody; import retrofit2.Call; -import retrofit2.http.Field; -import retrofit2.http.FormUrlEncoded; +import retrofit2.http.Body; import retrofit2.http.POST; -public interface TuskyAPI { - @FormUrlEncoded +public interface TuskyApi { @POST("/register") - Call register(@Field("instance_url") String instanceUrl, @Field("access_token") String accessToken, @Field("device_token") String deviceToken); - @FormUrlEncoded + Call register(@Body Session session); @POST("/unregister") - Call unregister(@Field("instance_url") String instanceUrl, @Field("access_token") String accessToken); + Call unregister(@Body Session session); } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountPagerAdapter.java b/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.java similarity index 84% rename from app/src/main/java/com/keylesspalace/tusky/AccountPagerAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.java index 505d89bd..8f04ae18 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountPagerAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.pager; import android.content.Context; import android.support.v4.app.Fragment; @@ -24,23 +24,27 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.fragment.AccountListFragment; +import com.keylesspalace.tusky.fragment.TimelineFragment; + import java.util.ArrayList; import java.util.List; -class AccountPagerAdapter extends FragmentPagerAdapter { +public class AccountPagerAdapter extends FragmentPagerAdapter { private Context context; private String accountId; private String[] pageTitles; private List registeredFragments; - AccountPagerAdapter(FragmentManager manager, Context context, String accountId) { + public AccountPagerAdapter(FragmentManager manager, Context context, String accountId) { super(manager); this.context = context; this.accountId = accountId; registeredFragments = new ArrayList<>(); } - void setPageTitles(String[] titles) { + public void setPageTitles(String[] titles) { pageTitles = titles; } @@ -72,7 +76,7 @@ class AccountPagerAdapter extends FragmentPagerAdapter { return pageTitles[position]; } - View getTabView(int position, ViewGroup root) { + public View getTabView(int position, ViewGroup root) { View view = LayoutInflater.from(context).inflate(R.layout.tab_account, root, false); TextView title = (TextView) view.findViewById(R.id.title); title.setText(pageTitles[position]); @@ -92,7 +96,7 @@ class AccountPagerAdapter extends FragmentPagerAdapter { super.destroyItem(container, position, object); } - List getRegisteredFragments() { + public List getRegisteredFragments() { return registeredFragments; } } diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelinePagerAdapter.java b/app/src/main/java/com/keylesspalace/tusky/pager/TimelinePagerAdapter.java similarity index 87% rename from app/src/main/java/com/keylesspalace/tusky/TimelinePagerAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/pager/TimelinePagerAdapter.java index 03dc88dc..82686041 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelinePagerAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/pager/TimelinePagerAdapter.java @@ -13,31 +13,34 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.pager; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.view.ViewGroup; +import com.keylesspalace.tusky.fragment.NotificationsFragment; +import com.keylesspalace.tusky.fragment.TimelineFragment; + import java.util.ArrayList; import java.util.List; -class TimelinePagerAdapter extends FragmentPagerAdapter { +public class TimelinePagerAdapter extends FragmentPagerAdapter { private int currentFragmentIndex; private List registeredFragments; - TimelinePagerAdapter(FragmentManager manager) { + public TimelinePagerAdapter(FragmentManager manager) { super(manager); currentFragmentIndex = 0; registeredFragments = new ArrayList<>(); } - Fragment getCurrentFragment() { + public Fragment getCurrentFragment() { return registeredFragments.get(currentFragmentIndex); } - List getRegisteredFragments() { + public List getRegisteredFragments() { return registeredFragments; } diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyTileService.java b/app/src/main/java/com/keylesspalace/tusky/service/TuskyTileService.java similarity index 94% rename from app/src/main/java/com/keylesspalace/tusky/TuskyTileService.java rename to app/src/main/java/com/keylesspalace/tusky/service/TuskyTileService.java index 720e9966..67789928 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyTileService.java +++ b/app/src/main/java/com/keylesspalace/tusky/service/TuskyTileService.java @@ -13,12 +13,14 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.service; import android.annotation.TargetApi; import android.content.Intent; import android.service.quicksettings.TileService; +import com.keylesspalace.tusky.ComposeActivity; + /** * Small Addition that adds in a QuickSettings tile that opens the Compose activity when clicked * Created by ztepps on 4/3/17. diff --git a/app/src/main/java/com/keylesspalace/tusky/Assert.java b/app/src/main/java/com/keylesspalace/tusky/util/Assert.java similarity index 85% rename from app/src/main/java/com/keylesspalace/tusky/Assert.java rename to app/src/main/java/com/keylesspalace/tusky/util/Assert.java index e185cfb0..c976184e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/Assert.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/Assert.java @@ -13,13 +13,15 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; + +import com.keylesspalace.tusky.BuildConfig; /** Android Studio complains about built-in assertions so this is an alternative. */ -class Assert { +public class Assert { private static boolean ENABLED = BuildConfig.DEBUG; - static void expect(boolean expression) { + public static void expect(boolean expression) { if (ENABLED && !expression) { throw new AssertionError(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/ConversationLineItemDecoration.java b/app/src/main/java/com/keylesspalace/tusky/util/ConversationLineItemDecoration.java similarity index 89% rename from app/src/main/java/com/keylesspalace/tusky/ConversationLineItemDecoration.java rename to app/src/main/java/com/keylesspalace/tusky/util/ConversationLineItemDecoration.java index e5fb8562..dab773c3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ConversationLineItemDecoration.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ConversationLineItemDecoration.java @@ -13,19 +13,17 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.content.Context; -import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.support.v7.widget.RecyclerView; -import android.util.TypedValue; import android.view.View; -import static android.util.TypedValue.COMPLEX_UNIT_DIP; +import com.keylesspalace.tusky.R; -class ConversationLineItemDecoration extends RecyclerView.ItemDecoration { +public class ConversationLineItemDecoration extends RecyclerView.ItemDecoration { private final Context mContext; private final Drawable mDivider; diff --git a/app/src/main/java/com/keylesspalace/tusky/CountUpDownLatch.java b/app/src/main/java/com/keylesspalace/tusky/util/CountUpDownLatch.java similarity index 78% rename from app/src/main/java/com/keylesspalace/tusky/CountUpDownLatch.java rename to app/src/main/java/com/keylesspalace/tusky/util/CountUpDownLatch.java index e0d95438..5791cf60 100644 --- a/app/src/main/java/com/keylesspalace/tusky/CountUpDownLatch.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/CountUpDownLatch.java @@ -13,26 +13,26 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; -class CountUpDownLatch { +public class CountUpDownLatch { private int count; - CountUpDownLatch() { + public CountUpDownLatch() { this.count = 0; } - synchronized void countDown() { + public synchronized void countDown() { count--; notifyAll(); } - synchronized void countUp() { + public synchronized void countUp() { count++; notifyAll(); } - synchronized void await() throws InterruptedException { + public synchronized void await() throws InterruptedException { while (count != 0) { wait(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/CustomTabURLSpan.java b/app/src/main/java/com/keylesspalace/tusky/util/CustomTabURLSpan.java similarity index 96% rename from app/src/main/java/com/keylesspalace/tusky/CustomTabURLSpan.java rename to app/src/main/java/com/keylesspalace/tusky/util/CustomTabURLSpan.java index 7af9a84f..bef07fcf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/CustomTabURLSpan.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomTabURLSpan.java @@ -1,4 +1,4 @@ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.content.ActivityNotFoundException; import android.content.Context; @@ -11,6 +11,8 @@ import android.support.v4.content.ContextCompat; import android.text.style.URLSpan; import android.view.View; +import com.keylesspalace.tusky.R; + class CustomTabURLSpan extends URLSpan { CustomTabURLSpan(String url) { super(url); diff --git a/app/src/main/java/com/keylesspalace/tusky/CustomTabsHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/CustomTabsHelper.java similarity index 99% rename from app/src/main/java/com/keylesspalace/tusky/CustomTabsHelper.java rename to app/src/main/java/com/keylesspalace/tusky/util/CustomTabsHelper.java index 45670863..f5add047 100644 --- a/app/src/main/java/com/keylesspalace/tusky/CustomTabsHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomTabsHelper.java @@ -1,4 +1,4 @@ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; diff --git a/app/src/main/java/com/keylesspalace/tusky/DateUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/DateUtils.java similarity index 92% rename from app/src/main/java/com/keylesspalace/tusky/DateUtils.java rename to app/src/main/java/com/keylesspalace/tusky/util/DateUtils.java index 16137f87..b2cc49a2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/DateUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/DateUtils.java @@ -13,12 +13,12 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; -class DateUtils { +public class DateUtils { /* This is a rough duplicate of android.text.format.DateUtils.getRelativeTimeSpanString, * but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough. */ - static String getRelativeTimeSpanString(long then, long now) { + public static String getRelativeTimeSpanString(long then, long now) { final long MINUTE = 60; final long HOUR = 60 * MINUTE; final long DAY = 24 * HOUR; diff --git a/app/src/main/java/com/keylesspalace/tusky/DownsizeImageTask.java b/app/src/main/java/com/keylesspalace/tusky/util/DownsizeImageTask.java similarity index 97% rename from app/src/main/java/com/keylesspalace/tusky/DownsizeImageTask.java rename to app/src/main/java/com/keylesspalace/tusky/util/DownsizeImageTask.java index 56d50ed9..0cbc35a8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/DownsizeImageTask.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/DownsizeImageTask.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.content.ContentResolver; import android.graphics.Bitmap; @@ -31,13 +31,13 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; -class DownsizeImageTask extends AsyncTask { +public class DownsizeImageTask extends AsyncTask { private int sizeLimit; private ContentResolver contentResolver; private Listener listener; private List resultList; - DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, Listener listener) { + public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, Listener listener) { this.sizeLimit = sizeLimit; this.contentResolver = contentResolver; this.listener = listener; @@ -219,7 +219,7 @@ class DownsizeImageTask extends AsyncTask { super.onPostExecute(successful); } - interface Listener { + public interface Listener { void onSuccess(List contentList); void onFailure(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/EditTextTyped.java b/app/src/main/java/com/keylesspalace/tusky/util/EditTextTyped.java similarity index 98% rename from app/src/main/java/com/keylesspalace/tusky/EditTextTyped.java rename to app/src/main/java/com/keylesspalace/tusky/util/EditTextTyped.java index 367180a2..c32f666f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditTextTyped.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/EditTextTyped.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.content.Context; import android.support.v13.view.inputmethod.EditorInfoCompat; diff --git a/app/src/main/java/com/keylesspalace/tusky/EndlessOnScrollListener.java b/app/src/main/java/com/keylesspalace/tusky/util/EndlessOnScrollListener.java similarity index 90% rename from app/src/main/java/com/keylesspalace/tusky/EndlessOnScrollListener.java rename to app/src/main/java/com/keylesspalace/tusky/util/EndlessOnScrollListener.java index 9c91a89a..0b149de0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EndlessOnScrollListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/EndlessOnScrollListener.java @@ -13,12 +13,12 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -abstract class EndlessOnScrollListener extends RecyclerView.OnScrollListener { +public abstract class EndlessOnScrollListener extends RecyclerView.OnScrollListener { private static final int VISIBLE_THRESHOLD = 15; private int currentPage; private int previousTotalItemCount; @@ -26,7 +26,7 @@ abstract class EndlessOnScrollListener extends RecyclerView.OnScrollListener { private int startingPageIndex; private LinearLayoutManager layoutManager; - EndlessOnScrollListener(LinearLayoutManager layoutManager) { + public EndlessOnScrollListener(LinearLayoutManager layoutManager) { this.layoutManager = layoutManager; currentPage = 0; previousTotalItemCount = 0; @@ -56,7 +56,7 @@ abstract class EndlessOnScrollListener extends RecyclerView.OnScrollListener { } } - void reset() { + public void reset() { currentPage = startingPageIndex; previousTotalItemCount = 0; loading = true; diff --git a/app/src/main/java/com/keylesspalace/tusky/FlowLayout.java b/app/src/main/java/com/keylesspalace/tusky/util/FlowLayout.java similarity index 98% rename from app/src/main/java/com/keylesspalace/tusky/FlowLayout.java rename to app/src/main/java/com/keylesspalace/tusky/util/FlowLayout.java index 44616643..cf1f9e68 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FlowLayout.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/FlowLayout.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.content.Context; import android.content.res.TypedArray; @@ -21,6 +21,8 @@ import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; +import com.keylesspalace.tusky.R; + public class FlowLayout extends ViewGroup { private int paddingHorizontal; // internal padding between child views private int paddingVertical; // diff --git a/app/src/main/java/com/keylesspalace/tusky/HtmlUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/HtmlUtils.java similarity index 98% rename from app/src/main/java/com/keylesspalace/tusky/HtmlUtils.java rename to app/src/main/java/com/keylesspalace/tusky/util/HtmlUtils.java index d37b4cd2..eee9bb44 100644 --- a/app/src/main/java/com/keylesspalace/tusky/HtmlUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/HtmlUtils.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.os.Build; import android.text.Html; diff --git a/app/src/main/java/com/keylesspalace/tusky/IOUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java similarity index 85% rename from app/src/main/java/com/keylesspalace/tusky/IOUtils.java rename to app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java index 76e53b82..2b17eeeb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/IOUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.support.annotation.Nullable; @@ -21,8 +21,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -class IOUtils { - static void closeQuietly(@Nullable InputStream stream) { +public class IOUtils { + public static void closeQuietly(@Nullable InputStream stream) { try { if (stream != null) { stream.close(); @@ -32,7 +32,7 @@ class IOUtils { } } - static void closeQuietly(@Nullable OutputStream stream) { + public static void closeQuietly(@Nullable OutputStream stream) { try { if (stream != null) { stream.close(); diff --git a/app/src/main/java/com/keylesspalace/tusky/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java similarity index 87% rename from app/src/main/java/com/keylesspalace/tusky/LinkHelper.java rename to app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java index ba9a591f..46ddd5f1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LinkHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java @@ -13,9 +13,8 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; -import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -26,14 +25,13 @@ import android.view.View; import android.widget.TextView; import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.interfaces.LinkListener; -class LinkHelper { - static void setClickableText(TextView view, Spanned content, - @Nullable Status.Mention[] mentions, - final LinkListener listener) { +public class LinkHelper { + public static void setClickableText(TextView view, Spanned content, + @Nullable Status.Mention[] mentions, boolean useCustomTabs, + final LinkListener listener) { SpannableStringBuilder builder = new SpannableStringBuilder(content); - boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(view.getContext()) - .getBoolean("customTabs", true); URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class); for (URLSpan span : urlSpans) { int start = builder.getSpanStart(span); diff --git a/app/src/main/java/com/keylesspalace/tusky/Log.java b/app/src/main/java/com/keylesspalace/tusky/util/Log.java similarity index 95% rename from app/src/main/java/com/keylesspalace/tusky/Log.java rename to app/src/main/java/com/keylesspalace/tusky/util/Log.java index d36544ee..26d3d85f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/Log.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/Log.java @@ -13,7 +13,9 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; + +import com.keylesspalace.tusky.BuildConfig; /**A wrapper for android.util.Log that allows for disabling logging, such as for release builds.*/ public class Log { diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationClearBroadcastReceiver.java b/app/src/main/java/com/keylesspalace/tusky/util/NotificationClearBroadcastReceiver.java similarity index 97% rename from app/src/main/java/com/keylesspalace/tusky/NotificationClearBroadcastReceiver.java rename to app/src/main/java/com/keylesspalace/tusky/util/NotificationClearBroadcastReceiver.java index 9508dac6..d18902e3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationClearBroadcastReceiver.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/NotificationClearBroadcastReceiver.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.content.BroadcastReceiver; import android.content.Context; diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationMaker.java b/app/src/main/java/com/keylesspalace/tusky/util/NotificationMaker.java similarity index 97% rename from app/src/main/java/com/keylesspalace/tusky/NotificationMaker.java rename to app/src/main/java/com/keylesspalace/tusky/util/NotificationMaker.java index 8c7bfb85..e5bc7003 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationMaker.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/NotificationMaker.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.app.NotificationManager; import android.app.PendingIntent; @@ -29,6 +29,8 @@ import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; +import com.keylesspalace.tusky.MainActivity; +import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Notification; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; @@ -36,8 +38,8 @@ import com.squareup.picasso.Target; import org.json.JSONArray; import org.json.JSONException; -class NotificationMaker { - static void make(final Context context, final int notifyId, Notification body) { +public class NotificationMaker { + public static void make(final Context context, final int notifyId, Notification body) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); final SharedPreferences notificationPreferences = context.getSharedPreferences( diff --git a/app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java similarity index 97% rename from app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java rename to app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java index a821df4d..c90002d2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java @@ -13,11 +13,13 @@ * You should have received a copy of the GNU Lesser General Public License along with Tusky. If * not, see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.os.Build; import android.support.annotation.NonNull; +import com.keylesspalace.tusky.BuildConfig; + import java.io.IOException; import java.net.InetAddress; import java.net.Socket; @@ -94,7 +96,7 @@ public class OkHttpUtils { public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); Request requestWithUserAgent = originalRequest.newBuilder() - .header("User-Agent", "Tusky/"+BuildConfig.VERSION_NAME+" Android/"+Build.VERSION.RELEASE) + .header("User-Agent", "Tusky/"+ BuildConfig.VERSION_NAME+" Android/"+Build.VERSION.RELEASE) .build(); return chain.proceed(requestWithUserAgent); } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/PushNotificationClient.java b/app/src/main/java/com/keylesspalace/tusky/util/PushNotificationClient.java new file mode 100644 index 00000000..f3df738f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/PushNotificationClient.java @@ -0,0 +1,237 @@ +package com.keylesspalace.tusky.util; + +import android.app.NotificationManager; +import android.content.Context; +import android.support.annotation.NonNull; +import android.text.Spanned; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Notification; +import com.keylesspalace.tusky.json.SpannedTypeAdapter; +import com.keylesspalace.tusky.json.StringWithEmoji; +import com.keylesspalace.tusky.json.StringWithEmojiTypeAdapter; + +import org.eclipse.paho.android.service.MqttAndroidClient; +import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions; +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import java.io.InputStream; +import java.util.ArrayDeque; +import java.util.ArrayList; + +import static android.content.Context.NOTIFICATION_SERVICE; + +public class PushNotificationClient { + private static final String TAG = "PushNotificationClient"; + private static final int NOTIFY_ID = 666; + + private static class QueuedAction { + enum Type { + SUBSCRIBE, + UNSUBSCRIBE, + DISCONNECT, + } + + Type type; + String topic; + + QueuedAction(Type type) { + this.type = type; + } + + QueuedAction(Type type, String topic) { + this.type = type; + this.topic = topic; + } + } + + private MqttAndroidClient mqttAndroidClient; + private ArrayDeque queuedActions; + private ArrayList subscribedTopics; + + public PushNotificationClient(final @NonNull Context applicationContext, + @NonNull String serverUri) { + queuedActions = new ArrayDeque<>(); + subscribedTopics = new ArrayList<>(); + + // Create the MQTT client. + String clientId = MqttClient.generateClientId(); + mqttAndroidClient = new MqttAndroidClient(applicationContext, serverUri, clientId); + mqttAndroidClient.setCallback(new MqttCallbackExtended() { + @Override + public void connectComplete(boolean reconnect, String serverURI) { + if (reconnect) { + flushQueuedActions(); + for (String topic : subscribedTopics) { + subscribeToTopic(topic); + } + } + } + + @Override + public void connectionLost(Throwable cause) { + onConnectionLost(); + } + + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + onMessageReceived(applicationContext, new String(message.getPayload())); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) { + // This client is read-only, so this is unused. + } + }); + } + + private void flushQueuedActions() { + while (!queuedActions.isEmpty()) { + QueuedAction action = queuedActions.pop(); + switch (action.type) { + case SUBSCRIBE: subscribeToTopic(action.topic); break; + case UNSUBSCRIBE: unsubscribeToTopic(action.topic); break; + case DISCONNECT: disconnect(); break; + } + } + } + + /** Connect to the MQTT broker. */ + public void connect(Context context) { + MqttConnectOptions options = new MqttConnectOptions(); + options.setAutomaticReconnect(true); + options.setCleanSession(false); + try { + String password = context.getString(R.string.tusky_api_keystore_password); + InputStream keystore = context.getResources().openRawResource(R.raw.keystore_tusky_api); + try { + options.setSocketFactory(mqttAndroidClient.getSSLSocketFactory(keystore, password)); + } finally { + IOUtils.closeQuietly(keystore); + } + mqttAndroidClient.connect(options).setActionCallback(new IMqttActionListener() { + @Override + public void onSuccess(IMqttToken asyncActionToken) { + DisconnectedBufferOptions bufferOptions = new DisconnectedBufferOptions(); + bufferOptions.setBufferEnabled(true); + bufferOptions.setBufferSize(100); + bufferOptions.setPersistBuffer(false); + bufferOptions.setDeleteOldestMessages(false); + mqttAndroidClient.setBufferOpts(bufferOptions); + onConnectionSuccess(); + flushQueuedActions(); + } + + @Override + public void onFailure(IMqttToken asyncActionToken, Throwable exception) { + Log.e(TAG, "An exception occurred while connecting. " + exception.getMessage() + + " " + exception.getCause()); + onConnectionFailure(); + } + }); + } catch (MqttException e) { + Log.e(TAG, "An exception occurred while connecpting. " + e.getMessage()); + onConnectionFailure(); + } + } + + private void onConnectionSuccess() { + Log.v(TAG, "The connection succeeded."); + } + + private void onConnectionFailure() { + Log.v(TAG, "The connection failed."); + } + + private void onConnectionLost() { + Log.v(TAG, "The connection was lost."); + } + + /** Disconnect from the MQTT broker. */ + public void disconnect() { + if (!mqttAndroidClient.isConnected()) { + queuedActions.add(new QueuedAction(QueuedAction.Type.DISCONNECT)); + return; + } + try { + mqttAndroidClient.disconnect(); + } catch (MqttException ex) { + Log.e(TAG, "An exception occurred while disconnecting."); + onDisconnectFailed(); + } + } + + private void onDisconnectFailed() { + Log.v(TAG, "Failed while disconnecting from the broker."); + } + + /** Subscribe to the push notification topic. */ + public void subscribeToTopic(final String topic) { + if (!mqttAndroidClient.isConnected()) { + queuedActions.add(new QueuedAction(QueuedAction.Type.SUBSCRIBE, topic)); + return; + } + try { + mqttAndroidClient.subscribe(topic, 0, null, new IMqttActionListener() { + @Override + public void onSuccess(IMqttToken asyncActionToken) { + subscribedTopics.add(topic); + onConnectionSuccess(); + } + + @Override + public void onFailure(IMqttToken asyncActionToken, Throwable exception) { + Log.e(TAG, "An exception occurred while subscribing." + exception.getMessage()); + onConnectionFailure(); + } + }); + } catch (MqttException e) { + Log.e(TAG, "An exception occurred while subscribing." + e.getMessage()); + onConnectionFailure(); + } + } + + /** Unsubscribe from the push notification topic. */ + public void unsubscribeToTopic(String topic) { + if (!mqttAndroidClient.isConnected()) { + queuedActions.add(new QueuedAction(QueuedAction.Type.UNSUBSCRIBE, topic)); + return; + } + try { + mqttAndroidClient.unsubscribe(topic); + subscribedTopics.remove(topic); + } catch (MqttException e) { + Log.e(TAG, "An exception occurred while unsubscribing." + e.getMessage()); + onConnectionFailure(); + } + } + + private void onMessageReceived(final Context context, String message) { + Log.v(TAG, "Notification received: " + message); + + Gson gson = new GsonBuilder() + .registerTypeAdapter(Spanned.class, new SpannedTypeAdapter()) + .registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter()) + .create(); + Notification notification = gson.fromJson(message, Notification.class); + + NotificationMaker.make(context, NOTIFY_ID, notification); + } + + public void clearNotifications(Context context) { + ((NotificationManager) (context.getSystemService(NOTIFICATION_SERVICE))).cancel(NOTIFY_ID); + } + + public String getDeviceToken() { + return mqttAndroidClient.getClientId(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/RoundedTransformation.java b/app/src/main/java/com/keylesspalace/tusky/util/RoundedTransformation.java similarity index 98% rename from app/src/main/java/com/keylesspalace/tusky/RoundedTransformation.java rename to app/src/main/java/com/keylesspalace/tusky/util/RoundedTransformation.java index bd4192b5..ff153d68 100644 --- a/app/src/main/java/com/keylesspalace/tusky/RoundedTransformation.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/RoundedTransformation.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.graphics.Bitmap; import android.graphics.BitmapShader; diff --git a/app/src/main/java/com/keylesspalace/tusky/SpanUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.java similarity index 97% rename from app/src/main/java/com/keylesspalace/tusky/SpanUtils.java rename to app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.java index 09936e45..81a82d2b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SpanUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.java @@ -13,13 +13,13 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.text.Spannable; import android.text.Spanned; import android.text.style.ForegroundColorSpan; -class SpanUtils { +public class SpanUtils { private static class FindCharsResult { int charIndex; int stringIndex; @@ -94,7 +94,7 @@ class SpanUtils { return length; } - static void highlightSpans(Spannable text, int colour) { + public static void highlightSpans(Spannable text, int colour) { // Strip all existing colour spans. int n = text.length(); ForegroundColorSpan[] oldSpans = text.getSpans(0, n, ForegroundColorSpan.class); diff --git a/app/src/main/java/com/keylesspalace/tusky/ThemeUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java similarity index 81% rename from app/src/main/java/com/keylesspalace/tusky/ThemeUtils.java rename to app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java index 6e7a908d..310341fa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ThemeUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky; +package com.keylesspalace.tusky.util; import android.content.Context; import android.graphics.Color; @@ -26,8 +26,8 @@ import android.support.v4.content.ContextCompat; import android.util.TypedValue; import android.widget.ImageView; -class ThemeUtils { - static Drawable getDrawable(Context context, @AttrRes int attribute, +public class ThemeUtils { + public static Drawable getDrawable(Context context, @AttrRes int attribute, @DrawableRes int fallbackDrawable) { TypedValue value = new TypedValue(); @DrawableRes int resourceId; @@ -39,7 +39,7 @@ class ThemeUtils { return ContextCompat.getDrawable(context, resourceId); } - static @DrawableRes int getDrawableId(Context context, @AttrRes int attribute, + public static @DrawableRes int getDrawableId(Context context, @AttrRes int attribute, @DrawableRes int fallbackDrawableId) { TypedValue value = new TypedValue(); if (context.getTheme().resolveAttribute(attribute, value, true)) { @@ -49,7 +49,7 @@ class ThemeUtils { } } - static @ColorInt int getColor(Context context, @AttrRes int attribute) { + public static @ColorInt int getColor(Context context, @AttrRes int attribute) { TypedValue value = new TypedValue(); if (context.getTheme().resolveAttribute(attribute, value, true)) { return value.data; @@ -58,11 +58,11 @@ class ThemeUtils { } } - static void setImageViewTint(ImageView view, @AttrRes int attribute) { + public static void setImageViewTint(ImageView view, @AttrRes int attribute) { view.setColorFilter(getColor(view.getContext(), attribute), PorterDuff.Mode.SRC_IN); } - static void setDrawableTint(Context context, Drawable drawable, @AttrRes int attribute) { + public static void setDrawableTint(Context context, Drawable drawable, @AttrRes int attribute) { drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/TimelineReceiver.java b/app/src/main/java/com/keylesspalace/tusky/util/TimelineReceiver.java new file mode 100644 index 00000000..f6acc0d9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/TimelineReceiver.java @@ -0,0 +1,41 @@ +package com.keylesspalace.tusky.util; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import com.keylesspalace.tusky.adapter.TimelineAdapter; +import com.keylesspalace.tusky.fragment.TimelineFragment; + +public class TimelineReceiver extends BroadcastReceiver { + public static final class Types { + public static final String UNFOLLOW_ACCOUNT = "UNFOLLOW_ACCOUNT"; + public static final String BLOCK_ACCOUNT = "BLOCK_ACCOUNT"; + public static final String MUTE_ACCOUNT = "MUTE_ACCOUNT"; + } + + TimelineAdapter adapter; + + public TimelineReceiver(TimelineAdapter adapter) { + super(); + this.adapter = adapter; + } + + @Override + public void onReceive(Context context, final Intent intent) { + String id = intent.getStringExtra("id"); + adapter.removeAllByAccountId(id); + } + + public static IntentFilter getFilter(TimelineFragment.Kind kind) { + IntentFilter intentFilter = new IntentFilter(); + if (kind == TimelineFragment.Kind.HOME) { + intentFilter.addAction(Types.UNFOLLOW_ACCOUNT); + } + intentFilter.addAction(Types.BLOCK_ACCOUNT); + intentFilter.addAction(Types.MUTE_ACCOUNT); + + return intentFilter; + } +} diff --git a/app/src/main/res/drawable/account_header_default.png b/app/src/main/res/drawable/account_header_default.png deleted file mode 100644 index c4e44ad2..00000000 Binary files a/app/src/main/res/drawable/account_header_default.png and /dev/null differ diff --git a/app/src/main/res/drawable/account_header_missing.xml b/app/src/main/res/drawable/account_header_default.xml similarity index 100% rename from app/src/main/res/drawable/account_header_missing.xml rename to app/src/main/res/drawable/account_header_default.xml diff --git a/app/src/main/res/drawable/background_splash.xml b/app/src/main/res/drawable/background_splash.xml new file mode 100644 index 00000000..548f87ba --- /dev/null +++ b/app/src/main/res/drawable/background_splash.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/elephant_friend.png b/app/src/main/res/drawable/elephant_friend.png index 3c5145ba..a4e5e3e6 100644 Binary files a/app/src/main/res/drawable/elephant_friend.png and b/app/src/main/res/drawable/elephant_friend.png differ diff --git a/app/src/main/res/drawable/ic_file_download_black_24dp.xml b/app/src/main/res/drawable/ic_file_download_black_24dp.xml new file mode 100644 index 00000000..f5f7221a --- /dev/null +++ b/app/src/main/res/drawable/ic_file_download_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/splash_pattern.png b/app/src/main/res/drawable/splash_pattern.png new file mode 100644 index 00000000..dcc6f647 Binary files /dev/null and b/app/src/main/res/drawable/splash_pattern.png differ diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100644 index 00000000..c36fd02b --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + +