Merge pull request #1 from Vavassor/master

xd
main
m4sk1n 7 years ago committed by GitHub
commit 1922ebcba2
  1. 6
      README.md
  2. 2
      app/.gitignore
  3. 36
      app/build.gradle
  4. 55
      app/google-services.json
  5. 12
      app/src/fdroid/AndroidManifest.xml
  6. 132
      app/src/fdroid/java/com/keylesspalace/tusky/MessagingService.java
  7. 18
      app/src/google/AndroidManifest.xml
  8. 121
      app/src/google/java/com/keylesspalace/tusky/MessagingService.java
  9. 83
      app/src/google/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java
  10. 32
      app/src/main/AndroidManifest.xml
  11. 108
      app/src/main/java/com/keylesspalace/tusky/AboutActivity.java
  12. 28
      app/src/main/java/com/keylesspalace/tusky/AccountActivity.java
  13. 2
      app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java
  14. 132
      app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
  15. 103
      app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java
  16. 5
      app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java
  17. 4
      app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java
  18. 26
      app/src/main/java/com/keylesspalace/tusky/LoginActivity.java
  19. 67
      app/src/main/java/com/keylesspalace/tusky/MainActivity.java
  20. 2
      app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java
  21. 4
      app/src/main/java/com/keylesspalace/tusky/ReportActivity.java
  22. 28
      app/src/main/java/com/keylesspalace/tusky/SplashActivity.java
  23. 4
      app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java
  24. 12
      app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java
  25. 4
      app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.java
  26. 13
      app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java
  27. 8
      app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java
  28. 8
      app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java
  29. 8
      app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java
  30. 4
      app/src/main/java/com/keylesspalace/tusky/adapter/FooterViewHolder.java
  31. 11
      app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java
  32. 19
      app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
  33. 18
      app/src/main/java/com/keylesspalace/tusky/adapter/ReportAdapter.java
  34. 42
      app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java
  35. 22
      app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java
  36. 26
      app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java
  37. 4
      app/src/main/java/com/keylesspalace/tusky/entity/Account.java
  38. 28
      app/src/main/java/com/keylesspalace/tusky/entity/Session.java
  39. 26
      app/src/main/java/com/keylesspalace/tusky/entity/Status.java
  40. 40
      app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java
  41. 4
      app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java
  42. 46
      app/src/main/java/com/keylesspalace/tusky/fragment/ComposeOptionsFragment.java
  43. 11
      app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
  44. 4
      app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java
  45. 15
      app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
  46. 109
      app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java
  47. 92
      app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.java
  48. 47
      app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java
  49. 4
      app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.java
  50. 4
      app/src/main/java/com/keylesspalace/tusky/interfaces/AdapterItemRemover.java
  51. 4
      app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java
  52. 4
      app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java
  53. 4
      app/src/main/java/com/keylesspalace/tusky/interfaces/StatusRemoveListener.java
  54. 3
      app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.java
  55. 2
      app/src/main/java/com/keylesspalace/tusky/json/StringWithEmoji.java
  56. 4
      app/src/main/java/com/keylesspalace/tusky/json/StringWithEmojiTypeAdapter.java
  57. 2
      app/src/main/java/com/keylesspalace/tusky/network/MastodonAPI.java
  58. 15
      app/src/main/java/com/keylesspalace/tusky/network/TuskyApi.java
  59. 16
      app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.java
  60. 13
      app/src/main/java/com/keylesspalace/tusky/pager/TimelinePagerAdapter.java
  61. 4
      app/src/main/java/com/keylesspalace/tusky/service/TuskyTileService.java
  62. 8
      app/src/main/java/com/keylesspalace/tusky/util/Assert.java
  63. 8
      app/src/main/java/com/keylesspalace/tusky/util/ConversationLineItemDecoration.java
  64. 12
      app/src/main/java/com/keylesspalace/tusky/util/CountUpDownLatch.java
  65. 4
      app/src/main/java/com/keylesspalace/tusky/util/CustomTabURLSpan.java
  66. 2
      app/src/main/java/com/keylesspalace/tusky/util/CustomTabsHelper.java
  67. 6
      app/src/main/java/com/keylesspalace/tusky/util/DateUtils.java
  68. 8
      app/src/main/java/com/keylesspalace/tusky/util/DownsizeImageTask.java
  69. 2
      app/src/main/java/com/keylesspalace/tusky/util/EditTextTyped.java
  70. 8
      app/src/main/java/com/keylesspalace/tusky/util/EndlessOnScrollListener.java
  71. 4
      app/src/main/java/com/keylesspalace/tusky/util/FlowLayout.java
  72. 2
      app/src/main/java/com/keylesspalace/tusky/util/HtmlUtils.java
  73. 8
      app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java
  74. 14
      app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java
  75. 4
      app/src/main/java/com/keylesspalace/tusky/util/Log.java
  76. 2
      app/src/main/java/com/keylesspalace/tusky/util/NotificationClearBroadcastReceiver.java
  77. 8
      app/src/main/java/com/keylesspalace/tusky/util/NotificationMaker.java
  78. 6
      app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java
  79. 237
      app/src/main/java/com/keylesspalace/tusky/util/PushNotificationClient.java
  80. 2
      app/src/main/java/com/keylesspalace/tusky/util/RoundedTransformation.java
  81. 6
      app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.java
  82. 14
      app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java
  83. 41
      app/src/main/java/com/keylesspalace/tusky/util/TimelineReceiver.java
  84. BIN
      app/src/main/res/drawable/account_header_default.png
  85. 0
      app/src/main/res/drawable/account_header_default.xml
  86. 14
      app/src/main/res/drawable/background_splash.xml
  87. BIN
      app/src/main/res/drawable/elephant_friend.png
  88. 9
      app/src/main/res/drawable/ic_file_download_black_24dp.xml
  89. BIN
      app/src/main/res/drawable/splash_pattern.png
  90. 92
      app/src/main/res/layout/activity_about.xml
  91. 106
      app/src/main/res/layout/activity_account.xml
  92. 2
      app/src/main/res/layout/activity_compose.xml
  93. 4
      app/src/main/res/layout/activity_edit_profile.xml
  94. 84
      app/src/main/res/layout/activity_login.xml
  95. 19
      app/src/main/res/layout/activity_splash.xml
  96. 5
      app/src/main/res/layout/activity_view_video.xml
  97. 8
      app/src/main/res/layout/fragment_compose_options.xml
  98. 28
      app/src/main/res/layout/fragment_view_media.xml
  99. 4
      app/src/main/res/layout/item_status.xml
  100. 11
      app/src/main/res/menu/status_more_for_user.xml
  101. Some files were not shown because too many files have changed in this diff Show More

@ -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).
[<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="80" />](https://f-droid.org/repository/browse/?fdid=com.keylesspalace.tusky)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="80" />](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.

2
app/.gitignore vendored

@ -1,2 +1,4 @@
/build
app-release.apk
app-google-release.apk
src/main/res/raw/keystore_tusky_api.bks

@ -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'

@ -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"
}

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keylesspalace.tusky">
<application>
<service
android:name=".MessagingService"
android:enabled="true"
android:exported="true" />
</application>
</manifest>

@ -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 <http://www.gnu.org/licenses>. */
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<List<Notification>>() {
@Override
public void onResponse(Call<List<Notification>> call,
Response<List<Notification>> response) {
if (response.isSuccessful()) {
onNotificationsReceived(response.body());
}
}
@Override
public void onFailure(Call<List<Notification>> 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<Notification> notificationList) {
SharedPreferences notificationsPreferences = getSharedPreferences(
"Notifications", Context.MODE_PRIVATE);
Set<String> currentIds = notificationsPreferences.getStringSet(
"current_ids", new HashSet<String>());
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();
}
}

@ -1,18 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keylesspalace.tusky">
<application>
<meta-data android:name="firebase_analytics_collection_enabled" android:value="false" />
<service android:name=".MyFirebaseInstanceIdService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<service android:name=".MessagingService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

@ -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 <http://www.gnu.org/licenses>.
*
* 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<Notification>() {
@Override
public void onResponse(Call<Notification> call, Response<Notification> response) {
if (response.isSuccessful()) {
NotificationMaker.make(MessagingService.this, NOTIFY_ID, response.body());
}
}
@Override
public void onFailure(Call<Notification> 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);
}
}

@ -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 <http://www.gnu.org/licenses>.
*
* 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<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
Log.d(TAG, response.message());
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.d(TAG, t.getMessage());
}
});
tuskyAPI.register("https://" + domain, accessToken, refreshedToken).enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
Log.d(TAG, response.message());
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.d(TAG, t.getMessage());
}
});
}
}
}

@ -6,20 +6,23 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.VIBRATE" /> <!--For notifications-->
<uses-permission android:name="android.permission.WAKE_LOCK" /> <!--Required by Eclipse Paho-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!--Required by Eclipse Paho-->
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".TuskyApplication"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:name=".TuskyApplication">
android:theme="@style/AppTheme">
<activity android:name=".SplashActivity">
<activity
android:name=".SplashActivity"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
@ -37,7 +40,10 @@
android:scheme="@string/oauth_scheme" />
</intent-filter>
</activity>
<activity android:name=".MainActivity" />
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|keyboardHidden">
</activity>
<activity
android:name=".ComposeActivity"
android:windowSoftInputMode="stateVisible|adjustResize">
@ -57,14 +63,19 @@
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity android:name=".ViewVideoActivity" android:configChanges="orientation|keyboardHidden|screenSize" />
<activity android:name=".ViewThreadActivity" />
<activity
android:name=".ViewVideoActivity"
android:configChanges="orientation|keyboardHidden|screenSize" />
<activity
android:name=".ViewThreadActivity"
android:configChanges="orientation|screenSize" />
<activity android:name=".ViewTagActivity" />
<activity android:name=".AccountActivity" />
<activity android:name=".EditProfileActivity" />
<activity android:name=".PreferencesActivity" />
<activity android:name=".FavouritesActivity" />
<activity android:name=".AccountListActivity" />
<activity android:name=".AboutActivity" />
<activity
android:name=".ReportActivity"
android:windowSoftInputMode="stateVisible|adjustResize" />
@ -72,11 +83,12 @@
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
android:theme="@style/Base.Theme.AppCompat" />
<receiver android:name=".NotificationClearBroadcastReceiver" />
<receiver android:name=".util.NotificationClearBroadcastReceiver" />
<service android:name="org.eclipse.paho.android.service.MqttService" />
<service
tools:targetApi="24"
android:name="com.keylesspalace.tusky.TuskyTileService"
android:name="com.keylesspalace.tusky.service.TuskyTileService"
android:icon="@drawable/ic_send_24dp"
android:label="Compose Toot"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">

@ -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<List<Account>> callback = new Callback<List<Account>>() {
@Override
public void onResponse(Call<List<Account>> call, Response<List<Account>> response) {
if (response.isSuccessful()) {
List<Account> 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<List<Account>> 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);
}
}

@ -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<Relationship> call, Response<Relationship> 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<Relationship> call, Response<Relationship> 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) {

@ -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,

@ -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<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
Log.d(TAG, "Enable push notifications response: " + response.message());
Callback<ResponseBody> callback = new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call,
retrofit2.Response<ResponseBody> response) {
if (response.isSuccessful()) {
pushNotificationClient.subscribeToTopic(getPushNotificationTopic());
pushNotificationClient.connect(BaseActivity.this);
} else {
onEnablePushNotificationsFailure(response.message());
}
}
@Override
public void onFailure(Call<ResponseBody> 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<ResponseBody> 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<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
Log.d(TAG, "Disable push notifications response: " + response.message());
Callback<ResponseBody> callback = new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call,
retrofit2.Response<ResponseBody> response) {
if (response.isSuccessful()) {
pushNotificationClient.unsubscribeToTopic(getPushNotificationTopic());
} else {
onDisablePushNotificationsFailure();
}
}
@Override
public void onFailure(Call<ResponseBody> 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<ResponseBody> 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);
}
}

@ -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<Media> 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<Status>() {
@Override
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
public void onResponse(Call<Status> call, Response<Status> 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<Media> call, retrofit2.Response<Media> 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

@ -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);
}

@ -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;

@ -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<AccessToken> callback = new Callback<AccessToken>() {
@ -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<AccessToken> 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;
}

@ -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);
}
}

@ -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;

@ -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;

@ -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();
}
}

@ -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;

@ -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. */
}
}

@ -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);
}
});

@ -13,17 +13,18 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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<Account> accountList;
AccountActionListener accountActionListener;
@ -38,7 +39,7 @@ abstract class AccountAdapter extends RecyclerView.Adapter {
return accountList.size() + 1;
}
void update(List<Account> newAccounts) {
public void update(List<Account> newAccounts) {
if (newAccounts == null || newAccounts.isEmpty()) {
return;
}
@ -59,14 +60,14 @@ abstract class AccountAdapter extends RecyclerView.Adapter {
notifyDataSetChanged();
}
void addItems(List<Account> newAccounts) {
public void addItems(List<Account> 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;
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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);
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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);
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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);
}

@ -13,12 +13,14 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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);

@ -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);
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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<Notification> newNotifications) {
public void update(List<Notification> newNotifications) {
if (newNotifications == null || newNotifications.isEmpty()) {
return;
}
@ -200,7 +203,7 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
notifyDataSetChanged();
}
void addItems(List<Notification> new_notifications) {
public void addItems(List<Notification> 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);
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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<ReportStatus> 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<ReportStatus> newStatuses) {
public void addItems(List<ReportStatus> 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<String> idList = new ArrayList<>();
for (ReportStatus status : statusList) {
if (status.checked) {

@ -13,9 +13,10 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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

@ -13,25 +13,27 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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<Status> 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<Status> ancestors, List<Status> descendants) {
public void setContext(List<Status> ancestors, List<Status> 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

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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<Status> newStatuses) {
public void update(List<Status> newStatuses) {
if (newStatuses == null || newStatuses.isEmpty()) {
return;
}
@ -131,7 +134,7 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover
notifyDataSetChanged();
}
void addItems(List<Status> newStatuses) {
public void addItems(List<Status> 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);
}

@ -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;

@ -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 <http://www.gnu.org/licenses>. */
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;
}
}

@ -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<Type> {
@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;
}
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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) {

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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;

@ -13,12 +13,18 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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);
}
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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;

@ -13,11 +13,13 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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) {

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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);
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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<List<Status>> 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<List<Status>> cb = new Callback<List<Status>>() {
Callback<List<Status>> callback = new Callback<List<Status>>() {
@Override
public void onResponse(Call<List<Status>> call, retrofit2.Response<List<Status>> response) {
public void onResponse(Call<List<Status>> call, Response<List<Status>> response) {
if (response.isSuccessful()) {
onFetchTimelineSuccess(response.body(), fromId);
} else {
@ -240,6 +253,7 @@ public class TimelineFragment extends SFragment implements
}
};
Call<List<Status>> 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<Status> statuses) {
Iterator<Status> 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<Status> 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);
}
}

@ -13,12 +13,10 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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;
}
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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<Status> 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<Status> call = mastodonApi.status(id);
call.enqueue(new Callback<Status>() {
@Override
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
public void onResponse(Call<Status> call, Response<Status> 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<StatusContext> call = api.statusContext(id);
Call<StatusContext> call = mastodonApi.statusContext(id);
call.enqueue(new Callback<StatusContext>() {
@Override
public void onResponse(Call<StatusContext> call, retrofit2.Response<StatusContext> response) {
public void onResponse(Call<StatusContext> call, Response<StatusContext> response) {
if (response.isSuccessful()) {
swipeRefreshLayout.setRefreshing(false);
StatusContext context = response.body();

@ -13,9 +13,9 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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);

@ -13,8 +13,8 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.interfaces;
interface AdapterItemRemover {
public interface AdapterItemRemover {
void removeItem(int position);
}

@ -13,9 +13,9 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.interfaces;
interface LinkListener {
public interface LinkListener {
void onViewTag(String tag);
void onViewAccount(String id);
}

@ -13,13 +13,13 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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);

@ -13,8 +13,8 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.interfaces;
interface StatusRemoveListener {
public interface StatusRemoveListener {
void removePostsByUser(String accountId);
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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;

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.json;
/**
* This is just a wrapper class for a String.

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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<StringWithEmoji> {
public class StringWithEmojiTypeAdapter implements JsonDeserializer<StringWithEmoji> {
@Override
public StringWithEmoji deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.network;
import com.keylesspalace.tusky.entity.AccessToken;
import com.keylesspalace.tusky.entity.Account;

@ -13,19 +13,18 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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<ResponseBody> register(@Field("instance_url") String instanceUrl, @Field("access_token") String accessToken, @Field("device_token") String deviceToken);
@FormUrlEncoded
Call<ResponseBody> register(@Body Session session);
@POST("/unregister")
Call<ResponseBody> unregister(@Field("instance_url") String instanceUrl, @Field("access_token") String accessToken);
Call<ResponseBody> unregister(@Body Session session);
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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<Fragment> 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<Fragment> getRegisteredFragments() {
public List<Fragment> getRegisteredFragments() {
return registeredFragments;
}
}

@ -13,31 +13,34 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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<Fragment> 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<Fragment> getRegisteredFragments() {
public List<Fragment> getRegisteredFragments() {
return registeredFragments;
}

@ -13,12 +13,14 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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.

@ -13,13 +13,15 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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();
}

@ -13,19 +13,17 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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;

@ -13,26 +13,26 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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();
}

@ -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);

@ -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;

@ -13,12 +13,12 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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;

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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<Uri, Void, Boolean> {
public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
private int sizeLimit;
private ContentResolver contentResolver;
private Listener listener;
private List<byte[]> 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<Uri, Void, Boolean> {
super.onPostExecute(successful);
}
interface Listener {
public interface Listener {
void onSuccess(List<byte[]> contentList);
void onFailure();
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.util;
import android.content.Context;
import android.support.v13.view.inputmethod.EditorInfoCompat;

@ -13,12 +13,12 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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;

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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; //

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.util;
import android.os.Build;
import android.text.Html;

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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();

@ -13,9 +13,8 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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);

@ -13,7 +13,9 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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 {

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.util;
import android.content.BroadcastReceiver;
import android.content.Context;

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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(

@ -13,11 +13,13 @@
* You should have received a copy of the GNU Lesser General Public License along with Tusky. If
* not, see <http://www.gnu.org/licenses/>. */
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);
}

@ -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<QueuedAction> queuedActions;
private ArrayList<String> 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();
}
}

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.util;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;

@ -13,13 +13,13 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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);

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
package com.keylesspalace.tusky.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);
}
}

@ -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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap
android:src="@drawable/splash_pattern"
android:tileMode="repeat" />
</item>
<item>
<bitmap
android:src="@mipmap/ic_logo"
android:gravity="center"
android:tileMode="disabled" />
</item>
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 40 KiB

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.keylesspalace.tusky.AboutActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/toolbar_background_color"
android:elevation="4dp" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<com.mikhaellopez.circularfillableloaders.CircularFillableLoaders
android:id="@+id/circularFillableLoaders"
android:layout_width="200dp"
android:layout_height="200dp"
android:src="@mipmap/ic_logo"
app:cfl_border="true"
app:cfl_border_width="4dp"
app:cfl_progress="80"
app:cfl_wave_amplitude="0.08"
app:cfl_wave_color="?attr/splash_wave_color" />
<TextView
android:id="@+id/versionTV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:textIsSelectable="true"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/projectURL_TV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="web"
android:textIsSelectable="true"
android:padding="@dimen/text_content_margin"
android:text="@string/about_project_site"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/featuresURL_TV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="web"
android:textIsSelectable="true"
android:padding="@dimen/text_content_margin"
android:text="@string/about_bug_feature_request_site"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<Button
android:id="@+id/tusky_profile_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:padding="@dimen/text_content_margin"
android:text="@string/about_tusky_account"
android:textAlignment="center"
android:textAllCaps="false"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="@android:color/white" />
</LinearLayout>
</ScrollView>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>

@ -1,26 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:id="@+id/activity_account"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/activity_account"
android:fitsSystemWindows="true">
android:fitsSystemWindows="true"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:id="@+id/account_app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.Account.AppBarLayout"
android:id="@+id/account_app_bar_layout">
android:theme="@style/AppTheme.Account.AppBarLayout">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:contentScrim="?attr/toolbar_background_color"
android:fitsSystemWindows="true"
app:contentScrim="?attr/toolbar_background_color"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleEnabled="false">
<RelativeLayout
@ -29,93 +28,96 @@
android:background="?attr/account_header_background_color">
<ImageView
android:id="@+id/account_header"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/account_header"
android:scaleType="centerCrop"
app:layout_collapseMode="pin"
android:fitsSystemWindows="true"
android:layout_alignBottom="@+id/account_header_info"
android:layout_alignTop="@+id/account_header_info"
android:layout_alignBottom="@id/account_header_info"
android:contentDescription="@null" />
android:background="@drawable/account_header_default"
android:contentDescription="@null"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
app:layout_collapseMode="pin" />
<LinearLayout
android:orientation="vertical"
android:id="@+id/account_header_info"
android:paddingTop="?attr/actionBarSize"
android:background="@drawable/account_header_gradient"
android:layout_width="match_parent"
app:layout_collapseMode="parallax"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:background="@drawable/account_header_gradient"
android:orientation="vertical"
android:paddingTop="?attr/actionBarSize"
app:layout_collapseMode="parallax">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:layout_height="wrap_content">
android:paddingTop="16dp">
<com.pkmmte.view.CircularImageView
android:id="@+id/account_avatar"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginRight="10dp"
app:shadow="true"
android:id="@+id/account_avatar" />
android:src="@drawable/avatar_default"
app:shadow="true" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/account_avatar"
android:layout_alignParentRight="true">
android:orientation="vertical">
<TextView
android:id="@+id/account_display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/account_display_name"
android:maxLines="1"
android:ellipsize="end"
android:textStyle="normal|bold"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="18sp" />
android:textSize="18sp"
android:textStyle="normal|bold" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/account_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:textColor="?android:textColorSecondary"
android:id="@+id/account_username" />
android:maxLines="1"
android:textColor="?android:textColorSecondary" />
<ImageView
android:id="@+id/account_locked"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_width="16sp"
android:layout_height="16sp"
android:layout_centerVertical="true"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:layout_toEndOf="@id/account_username"
app:srcCompat="@drawable/reblog_disabled_light"
android:tint="?android:textColorSecondary"
android:layout_toRightOf="@id/account_username"
android:contentDescription="@string/description_account_locked" />
android:contentDescription="@string/description_account_locked"
android:tint="?android:textColorSecondary"
android:visibility="gone"
app:srcCompat="@drawable/reblog_disabled_light" />
</RelativeLayout>
</LinearLayout>
</RelativeLayout>
<TextView
android:id="@+id/account_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/account_note"
android:textColor="?android:textColorTertiary"
android:padding="16dp"
android:paddingTop="10dp" />
android:paddingTop="10dp"
android:textColor="?android:textColorTertiary" />
</LinearLayout>
@ -125,9 +127,9 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@android:color/transparent"
android:layout_gravity="top"
android:layout_alignParentTop="true"
android:layout_gravity="top"
android:background="@android:color/transparent"
app:layout_collapseMode="pin"
app:popupTheme="?attr/account_toolbar_popup_theme" />
@ -136,16 +138,16 @@
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/pager"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
app:tabBackground="?android:colorBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
app:tabBackground="?android:colorBackground">
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
@ -172,18 +174,18 @@
android:id="@+id/tab_bottom_shadow"
android:layout_width="match_parent"
android:layout_height="2dp"
app:layout_anchor="@id/tab_layout"
app:layout_anchorGravity="bottom"
android:background="@drawable/material_drawer_shadow_bottom"
android:visibility="visible" />
android:visibility="visible"
app:layout_anchor="@id/tab_layout"
app:layout_anchorGravity="bottom" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/floating_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_person_add_24dp"
android:contentDescription="@string/action_follow" />
android:contentDescription="@string/action_follow"
app:srcCompat="@drawable/ic_person_add_24dp" />
</android.support.design.widget.CoordinatorLayout>

@ -54,7 +54,7 @@
android:paddingLeft="16dp"
android:paddingRight="16dp">
<com.keylesspalace.tusky.EditTextTyped
<com.keylesspalace.tusky.util.EditTextTyped
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/compose_edit_field"

@ -86,7 +86,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="30dp">
<EditText
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edit_profile_display_name"
@ -103,7 +103,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="30dp">
<EditText
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edit_profile_note"

@ -14,45 +14,69 @@
android:layout_height="wrap_content">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="147dp"
android:layout_height="160dp"
android:layout_marginBottom="50dp"
android:src="@drawable/elephant_friend"
android:contentDescription="@null" />
<android.support.design.widget.TextInputLayout
<LinearLayout
android:id="@+id/login_input"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="250dp">
<EditText
android:layout_width="match_parent"
android:orientation="vertical">
<android.support.design.widget.TextInputLayout
android:layout_height="wrap_content"
android:inputType="textUri"
android:hint="@string/hint_domain"
android:ems="10"
android:id="@+id/edit_text_domain" />
</android.support.design.widget.TextInputLayout>
<Button
android:id="@+id/button_login"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@android:color/white"
android:text="@string/action_login" />
android:layout_width="250dp">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:hint="@string/hint_domain"
android:ems="10"
android:id="@+id/edit_text_domain" />
</android.support.design.widget.TextInputLayout>
<TextView
android:layout_width="250dp"
android:layout_height="wrap_content"
<Button
android:id="@+id/button_login"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@android:color/white"
android:text="@string/action_login" />
<TextView
android:layout_width="250dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:id="@+id/text_error" />
<TextView
android:layout_width="250dp"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:textAlignment="center"
android:id="@+id/whats_an_instance"
android:text="@string/link_whats_an_instance" />
</LinearLayout>
<LinearLayout
android:id="@+id/login_loading"
android:visibility="gone"
android:id="@+id/text_error" />
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ProgressBar
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:paddingTop="10dp"
android:textAlignment="center"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:text="@string/login_connection"/>
</LinearLayout>
<TextView
android:layout_width="250dp"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:textAlignment="center"
android:id="@+id/whats_an_instance"
android:text="@string/link_whats_an_instance" />
</LinearLayout>
</ScrollView>

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center"
android:orientation="vertical"
android:background="?attr/splash_background_color"
android:layout_height="match_parent">
<com.mikhaellopez.circularfillableloaders.CircularFillableLoaders
android:id="@+id/circularFillableLoaders"
android:layout_width="200dp"
android:layout_height="200dp"
android:src="@mipmap/ic_logo"
app:cfl_border="true"
app:cfl_border_width="4dp"
app:cfl_progress="80"
app:cfl_wave_amplitude="0.08"
app:cfl_wave_color="?attr/splash_wave_color" />
</LinearLayout>

@ -14,6 +14,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" />
<ProgressBar
android:id="@+id/video_progress"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"

@ -15,48 +15,40 @@
<RadioButton
android:text="@string/visibility_public"
android:button="@drawable/ic_public_24dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_public"
android:layout_marginBottom="5dp"
android:paddingLeft="10dp"
android:buttonTint="@color/drawer_visibility_panel_item"
android:layout_weight="1" />
<RadioButton
android:text="@string/visibility_unlisted"
android:button="@drawable/ic_lock_open_24dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_unlisted"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:paddingLeft="10dp"
android:buttonTint="@color/drawer_visibility_panel_item"
android:layout_weight="1" />
<RadioButton
android:text="@string/visibility_private"
android:button="@drawable/ic_lock_outline_24dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_private"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:paddingLeft="10dp"
android:buttonTint="@color/drawer_visibility_panel_item"
android:layout_weight="1" />
<RadioButton
android:text="@string/visibility_direct"
android:button="@drawable/ic_email_24dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_direct"
android:layout_marginTop="5dp"
android:paddingLeft="10dp"
android:buttonTint="@color/drawer_visibility_panel_item"
android:layout_weight="1" />
</RadioGroup>

@ -1,12 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:layout_gravity="center"
android:background="@android:color/black">
<uk.co.senab.photoview.PhotoView
android:background="@android:color/black"
android:clickable="true">
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/view_media_image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/toolbar_view_media"
app:navigationIcon="?attr/homeAsUpIndicator" />
<ProgressBar
android:id="@+id/view_media_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_gravity="center" />
</RelativeLayout>

@ -95,7 +95,7 @@
</RelativeLayout>
<com.keylesspalace.tusky.FlowLayout
<com.keylesspalace.tusky.util.FlowLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/status_content_warning_bar"
@ -127,7 +127,7 @@
android:textAllCaps="true"
android:background="?attr/content_warning_button" />
</com.keylesspalace.tusky.FlowLayout>
</com.keylesspalace.tusky.util.FlowLayout>
<TextView
android:id="@+id/status_content"

@ -2,7 +2,16 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/status_share"
android:title="@string/action_share"/>
android:title="@string/action_share">
<menu>
<item
android:id="@+id/status_share_link"
android:title="@string/status_share_link" />
<item
android:id="@+id/status_share_content"
android:title="@string/status_share_content"/>
</menu>
</item>
<item android:title="@string/action_delete"
android:id="@+id/status_delete" />
</menu>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save