Push notifications

This commit is contained in:
Eugen Rochko 2017-03-12 08:31:20 +01:00
parent 03fb9f45b2
commit 2bbd46e841
13 changed files with 420 additions and 360 deletions

View File

@ -25,6 +25,9 @@ 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') {
transitive = true
}
compile 'com.android.support:appcompat-v7:25.2.0'
compile 'com.android.support:recyclerview-v7:25.2.0'
compile 'com.android.support:support-v13:25.2.0'
@ -32,16 +35,17 @@ dependencies {
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.pkmmte.view:circularimageview:1.1'
compile 'com.github.peter9870:sparkbutton:master'
testCompile 'junit:junit:4.12'
compile 'com.mikhaellopez:circularfillableloaders:1.2.0'
compile 'com.squareup.retrofit2:retrofit:2.2.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile('com.mikepenz:materialdrawer:5.8.2@aar') {
transitive = true
}
compile 'com.github.chrisbanes:PhotoView:1.3.1'
compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar'
compile 'com.github.arimorty:floatingsearchview:2.0.3'
compile 'com.jakewharton:butterknife:8.4.0'
compile 'com.google.firebase:firebase-messaging:10.0.1'
testCompile 'junit:junit:4.12'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
}
apply plugin: 'com.google.gms.google-services'

View File

@ -4,7 +4,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:allowBackup="true"
@ -17,15 +17,20 @@
android:theme="@android:style/Theme.Black.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".LoginActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/oauth_scheme" android:host="@string/oauth_redirect_host" />
<data
android:host="@string/oauth_redirect_host"
android:scheme="@string/oauth_scheme" />
</intent-filter>
</activity>
<activity android:name=".MainActivity" />
@ -42,10 +47,17 @@
<activity
android:name=".ReportActivity"
android:windowSoftInputMode="stateVisible|adjustResize" />
<service
android:name=".PullNotificationService"
android:description="@string/notification_service_description"
android:exported="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=".MyFirebaseMessagingService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
</application>
</manifest>

View File

@ -47,12 +47,14 @@ import retrofit2.converter.gson.GsonConverterFactory;
* activity extend from it. */
public class BaseActivity extends AppCompatActivity {
protected MastodonAPI mastodonAPI;
protected TuskyAPI tuskyAPI;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
createMastodonAPI();
createTuskyAPI();
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) {
setTheme(R.style.AppTheme_Light);
@ -121,6 +123,14 @@ public class BaseActivity extends AppCompatActivity {
mastodonAPI = retrofit.create(MastodonAPI.class);
}
protected void createTuskyAPI() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getString(R.string.tusky_api_url))
.build();
tuskyAPI = retrofit.create(TuskyAPI.class);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
TypedValue value = new TypedValue();

View File

@ -41,6 +41,7 @@ import android.widget.TextView;
import com.arlib.floatingsearchview.FloatingSearchView;
import com.arlib.floatingsearchview.suggestions.SearchSuggestionsAdapter;
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion;
import com.google.firebase.iid.FirebaseInstanceId;
import com.keylesspalace.tusky.entity.Account;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.materialdrawer.AccountHeader;
@ -60,6 +61,9 @@ import com.squareup.picasso.Picasso;
import java.util.List;
import java.util.Stack;
import butterknife.BindView;
import butterknife.ButterKnife;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -67,28 +71,27 @@ import retrofit2.Response;
public class MainActivity extends BaseActivity {
private static final String TAG = "MainActivity"; // logging tag and Volley request tag
private AlarmManager alarmManager;
private PendingIntent serviceAlarmIntent;
private boolean notificationServiceEnabled;
private String loggedInAccountId;
private String loggedInAccountUsername;
Stack<Integer> pageHistory = new Stack<Integer>();
private ViewPager viewPager;
private AccountHeader headerResult;
private Drawer drawer;
@BindView(R.id.floating_search_view) FloatingSearchView searchView;
@BindView(R.id.floating_btn) FloatingActionButton floatingBtn;
@BindView(R.id.tab_layout) TabLayout tabLayout;
@BindView(R.id.pager) ViewPager viewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
// Fetch user info while we're doing other things.
fetchUserInfo();
//Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
//setSupportActionBar(toolbar);
FloatingActionButton floatingBtn = (FloatingActionButton) findViewById(R.id.floating_btn);
floatingBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -97,8 +100,79 @@ public class MainActivity extends BaseActivity {
}
});
final FloatingSearchView searchView = (FloatingSearchView) findViewById(R.id.floating_search_view);
setupDrawer();
setupSearchView();
// Setup the tabs and timeline pager.
TimelinePagerAdapter adapter = new TimelinePagerAdapter(getSupportFragmentManager());
String[] pageTitles = {
getString(R.string.title_home),
getString(R.string.title_notifications),
getString(R.string.title_public)
};
adapter.setPageTitles(pageTitles);
int pageMargin = getResources().getDimensionPixelSize(R.dimen.tab_page_margin);
viewPager.setPageMargin(pageMargin);
Drawable pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
R.drawable.tab_page_margin_dark);
viewPager.setPageMarginDrawable(pageMarginDrawable);
viewPager.setAdapter(adapter);
tabLayout.setupWithViewPager(viewPager);
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
viewPager.setCurrentItem(tab.getPosition());
if (pageHistory.empty()) {
pageHistory.push(0);
}
if (pageHistory.contains(tab.getPosition())) {
pageHistory.remove(pageHistory.indexOf(tab.getPosition()));
}
pageHistory.push(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
Intent intent = getIntent();
if (intent != null) {
int tabPosition = intent.getIntExtra("tab_position", 0);
if (tabPosition != 0) {
tabLayout.getTabAt(tabPosition).select();
}
}
// Setup push notifications
tuskyAPI.register(getBaseUrl(), getAccessToken(), FirebaseInstanceId.getInstance().getToken()).enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
}
private void setupDrawer() {
headerResult = new AccountHeaderBuilder()
.withActivity(this)
.withSelectionListEnabledForSingleProfile(false)
@ -152,18 +226,7 @@ public class MainActivity extends BaseActivity {
Intent intent = new Intent(MainActivity.this, PreferencesActivity.class);
startActivity(intent);
} else if (drawerItemIdentifier == 4) {
if (notificationServiceEnabled) {
alarmManager.cancel(serviceAlarmIntent);
}
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.remove("domain");
editor.remove("accessToken");
editor.apply();
Intent intent = new Intent(MainActivity.this, SplashActivity.class);
startActivity(intent);
finish();
logout();
}
}
@ -171,7 +234,33 @@ public class MainActivity extends BaseActivity {
}
})
.build();
}
private void logout() {
tuskyAPI.unregister(getBaseUrl(), getAccessToken()).enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.remove("domain");
editor.remove("accessToken");
editor.apply();
Intent intent = new Intent(MainActivity.this, LoginActivity.class);
startActivity(intent);
finish();
}
private void setupSearchView() {
searchView.attachNavigationDrawerToMenuButton(drawer.getDrawerLayout());
searchView.setOnQueryChangeListener(new FloatingSearchView.OnQueryChangeListener() {
@ -237,70 +326,6 @@ public class MainActivity extends BaseActivity {
textView.setEllipsize(TextUtils.TruncateAt.END);
}
});
// Setup the tabs and timeline pager.
TimelinePagerAdapter adapter = new TimelinePagerAdapter(getSupportFragmentManager());
String[] pageTitles = {
getString(R.string.title_home),
getString(R.string.title_notifications),
getString(R.string.title_public)
};
adapter.setPageTitles(pageTitles);
viewPager = (ViewPager) findViewById(R.id.pager);
int pageMargin = getResources().getDimensionPixelSize(R.dimen.tab_page_margin);
viewPager.setPageMargin(pageMargin);
Drawable pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
R.drawable.tab_page_margin_dark);
viewPager.setPageMarginDrawable(pageMarginDrawable);
viewPager.setAdapter(adapter);
TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout);
tabLayout.setupWithViewPager(viewPager);
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
viewPager.setCurrentItem(tab.getPosition());
if (pageHistory.empty()) {
pageHistory.push(0);
}
if (pageHistory.contains(tab.getPosition())) {
pageHistory.remove(pageHistory.indexOf(tab.getPosition()));
}
pageHistory.push(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
// Retrieve notification update preference.
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
notificationServiceEnabled = preferences.getBoolean("pullNotifications", true);
String minutesString = preferences.getString("pullNotificationCheckInterval", "15");
long notificationCheckInterval = 60 * 1000 * Integer.valueOf(minutesString);
// Start up the PullNotificationsService.
alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(this, PullNotificationService.class);
final int SERVICE_REQUEST_CODE = 8574603; // This number is arbitrary.
serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
if (notificationServiceEnabled) {
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime(), notificationCheckInterval, serviceAlarmIntent);
} else {
alarmManager.cancel(serviceAlarmIntent);
}
}
private void fetchUserInfo() {

View File

@ -0,0 +1,61 @@
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 TuskyAPI tuskyAPI;
protected void createTuskyAPI() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getString(R.string.tusky_api_url))
.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) {
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
tuskyAPI.register("https://" + domain, accessToken, refreshedToken).enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
}
}
}

View File

@ -0,0 +1,189 @@
package com.keylesspalace.tusky;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
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 com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
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 MyFirebaseMessagingService extends FirebaseMessagingService {
private MastodonAPI mastodonAPI;
private static final String TAG = "MyFirebaseMessagingService";
@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);
createMastodonAPI();
mastodonAPI.notification(notificationId).enqueue(new Callback<Notification>() {
@Override
public void onResponse(Call<Notification> call, Response<Notification> response) {
buildNotification(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 = new OkHttpClient.Builder()
.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())
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + domain)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
mastodonAPI = retrofit.create(MastodonAPI.class);
}
private String truncateWithEllipses(String string, int limit) {
if (string.length() < limit) {
return string;
} else {
return string.substring(0, limit - 3) + "...";
}
}
private void buildNotification(Notification body) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
Intent resultIntent = new Intent(this, MainActivity.class);
resultIntent.putExtra("tab_position", 1);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_notify)
.setAutoCancel(true)
.setContentIntent(resultPendingIntent);
final Integer mId = (int)(System.currentTimeMillis() / 1000);
Target mTarget = new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
builder.setLargeIcon(bitmap);
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))).notify(mId, builder.build());
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
}
};
Picasso.with(this)
.load(body.account.avatar)
.placeholder(R.drawable.avatar_default)
.into(mTarget);
if (preferences.getBoolean("notificationAlertSound", true)) {
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
}
if (preferences.getBoolean("notificationStyleVibrate", false)) {
builder.setVibrate(new long[] { 500, 500 });
}
if (preferences.getBoolean("notificationStyleLight", false)) {
builder.setLights(0xFF00FF8F, 300, 1000);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.setVisibility(android.app.Notification.VISIBILITY_PRIVATE);
builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
}
switch (body.type) {
case MENTION:
builder.setContentTitle(String.format(getString(R.string.notification_mention_format), body.account.getDisplayName()))
.setContentText(truncateWithEllipses(body.status.content.toString(), 40));
break;
case FOLLOW:
builder.setContentTitle(String.format(getString(R.string.notification_follow_format), body.account.getDisplayName()))
.setContentText(truncateWithEllipses(body.account.username, 40));
break;
case FAVOURITE:
builder.setContentTitle(String.format(getString(R.string.notification_favourite_format), body.account.getDisplayName()))
.setContentText(truncateWithEllipses(body.status.content.toString(), 40));
break;
case REBLOG:
builder.setContentTitle(String.format(getString(R.string.notification_reblog_format), body.account.getDisplayName()))
.setContentText(truncateWithEllipses(body.status.content.toString(), 40));
break;
}
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))).notify(mId, builder.build());
}
}

View File

@ -117,15 +117,6 @@ public class NotificationsFragment extends SFragment implements
return rootView;
}
@Override
public void onResume() {
super.onResume();
// When we view this fragment, dismiss the notifications
NotificationManager notificationManager = (NotificationManager) getActivity().getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(PullNotificationService.NOTIFY_ID);
}
@Override
public void onDestroyView() {
TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout);

View File

@ -1,233 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is part of Tusky.
*
* Tusky 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.*;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.Spanned;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.keylesspalace.tusky.entity.*;
import com.keylesspalace.tusky.entity.Notification;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class PullNotificationService extends IntentService {
static final int NOTIFY_ID = 6; // This is an arbitrary number.
private static final String TAG = "PullNotifications"; // logging tag
public PullNotificationService() {
super("Tusky Pull Notification Service");
}
@Override
protected void onHandleIntent(Intent intent) {
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
String domain = preferences.getString("domain", null);
String accessToken = preferences.getString("accessToken", null);
String lastUpdateId = preferences.getString("lastUpdateId", null);
checkNotifications(domain, accessToken, lastUpdateId);
}
private void checkNotifications(final String domain, final String accessToken,
final String lastUpdateId) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.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())
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + domain)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
MastodonAPI api = retrofit.create(MastodonAPI.class);
api.notifications(null, lastUpdateId, null).enqueue(new Callback<List<Notification>>() {
@Override
public void onResponse(Call<List<Notification>> call, retrofit2.Response<List<Notification>> response) {
onCheckNotificationsSuccess(response.body(), lastUpdateId);
}
@Override
public void onFailure(Call<List<Notification>> call, Throwable t) {
onCheckNotificationsFailure((Exception) t);
}
});
}
private void onCheckNotificationsSuccess(List<com.keylesspalace.tusky.entity.Notification> notifications, String lastUpdateId) {
List<MentionResult> mentions = new ArrayList<>();
for (com.keylesspalace.tusky.entity.Notification notification : notifications) {
if (notification.type == com.keylesspalace.tusky.entity.Notification.Type.MENTION) {
Status status = notification.status;
if (status != null) {
MentionResult mention = new MentionResult();
mention.content = status.content.toString();
mention.displayName = notification.account.getDisplayName();
mention.avatarUrl = status.account.avatar;
mentions.add(mention);
}
}
}
if (notifications.size() > 0) {
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("lastUpdateId", notifications.get(0).id);
editor.apply();
}
if (mentions.size() > 0) {
loadAvatar(mentions, mentions.get(0).avatarUrl);
}
}
private void onCheckNotificationsFailure(Exception exception) {
Log.e(TAG, "Failed to check notifications. " + exception.getMessage());
}
private static class MentionResult {
String displayName;
String content;
String avatarUrl;
}
private String truncateWithEllipses(String string, int limit) {
if (string.length() < limit) {
return string;
} else {
return string.substring(0, limit - 3) + "...";
}
}
private void loadAvatar(final List<MentionResult> mentions, String url) {
if (url != null) {
Target target = new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
updateNotification(mentions, bitmap);
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
updateNotification(mentions, null);
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {}
};
Picasso.with(this)
.load(url)
.into(target);
} else {
updateNotification(mentions, null);
}
}
private void updateNotification(List<MentionResult> mentions, @Nullable Bitmap icon) {
final int NOTIFICATION_CONTENT_LIMIT = 40;
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
String title;
if (mentions.size() > 1) {
title = String.format(
getString(R.string.notification_service_several_mentions),
mentions.size());
} else {
title = String.format(
getString(R.string.notification_service_one_mention),
mentions.get(0).displayName);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle(title);
if (icon != null) {
builder.setLargeIcon(icon);
}
if (preferences.getBoolean("notificationAlertSound", true)) {
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
}
if (preferences.getBoolean("notificationStyleVibrate", false)) {
builder.setVibrate(new long[] { 500, 500 });
}
if (preferences.getBoolean("notificationStyleLight", false)) {
builder.setLights(0xFF00FF8F, 300, 1000);
}
for (int i = 0; i < mentions.size(); i++) {
MentionResult mention = mentions.get(i);
String text = truncateWithEllipses(mention.content, NOTIFICATION_CONTENT_LIMIT);
builder.setContentText(text)
.setNumber(i);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.setVisibility(android.app.Notification.VISIBILITY_PRIVATE);
builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
}
Intent resultIntent = new Intent(this, SplashActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(SplashActivity.class);
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent =
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(resultPendingIntent);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFY_ID, builder.build());
}
}

View File

@ -0,0 +1,16 @@
package com.keylesspalace.tusky;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;
public interface TuskyAPI {
@FormUrlEncoded
@POST("/register")
Call<ResponseBody> register(@Field("instance_url") String instanceUrl, @Field("access_token") String accessToken, @Field("device_token") String deviceToken);
@FormUrlEncoded
@POST("/unregister")
Call<ResponseBody> unregister(@Field("instance_url") String instanceUrl, @Field("access_token") String accessToken);
}

View File

@ -2,6 +2,7 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:background="#60000000">
<uk.co.senab.photoview.PhotoView
android:id="@+id/view_media_image"

View File

@ -50,8 +50,8 @@
<string name="footer_end_of_notifications">end of the notifications</string>
<string name="footer_end_of_accounts">end of the accounts</string>
<string name="notification_reblog_format">%s boosted your status</string>
<string name="notification_favourite_format">%s favourited your status</string>
<string name="notification_reblog_format">%s boosted your toot</string>
<string name="notification_favourite_format">%s favourited your toot</string>
<string name="notification_follow_format">%s followed you</string>
<string name="report_username_format">Report @%s</string>
@ -129,5 +129,7 @@
<string name="search">Search accounts…</string>
<string name="toggle_nsfw">NSFW</string>
<string name="action_mention">Mention</string>
<string name="tusky_api_url">http://tusky.zeonfederated.com</string>
<string name="notification_mention_format">%s mentioned you</string>
</resources>

View File

@ -3,39 +3,20 @@
android:key="@string/preferences_file_key">
<PreferenceCategory android:title="@string/pref_title_notification_settings">
<CheckBoxPreference
android:key="pullNotifications"
android:title="@string/pref_title_pull_notifications"
android:summary="@string/pref_summary_pull_notifications"
android:defaultValue="true" />
<ListPreference
android:dependency="pullNotifications"
android:key="pullNotificationCheckInterval"
android:title="@string/pref_title_pull_notification_check_interval"
android:summary="@string/pref_summary_pull_notification_check_interval"
android:entries="@array/pull_notification_check_interval_names"
android:entryValues="@array/pull_notification_check_intervals"
android:defaultValue="15" />
<CheckBoxPreference
android:dependency="pullNotifications"
android:key="notificationAlertSound"
android:title="@string/pref_title_notification_alert_sound"
android:defaultValue="true" />
<CheckBoxPreference
android:dependency="pullNotifications"
android:key="notificationStyleVibrate"
android:title="@string/pref_title_notification_style_vibrate"
android:defaultValue="false" />
android:defaultValue="true" />
<CheckBoxPreference
android:dependency="pullNotifications"
android:key="notificationStyleLight"
android:title="@string/pref_title_notification_style_light"
android:defaultValue="false" />
android:defaultValue="true" />
</PreferenceCategory>

View File

@ -9,6 +9,7 @@ buildscript {
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath 'com.google.gms:google-services:3.0.0'
}
}