Merge branch 'master' into log-improvement

This commit is contained in:
Konrad Pozniak 2017-05-25 19:17:20 +02:00 committed by GitHub
commit 7501fcaeaa
17 changed files with 362 additions and 527 deletions

View file

@ -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;
@ -35,12 +32,14 @@ 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.OkHttpUtils;
import com.keylesspalace.tusky.util.PushNotificationClient;
import java.io.IOException;
@ -59,9 +58,9 @@ public class BaseActivity extends AppCompatActivity {
private static final String TAG = "BaseActivity"; // logging tag
public MastodonAPI mastodonAPI;
protected TuskyAPI tuskyAPI;
public TuskyApi tuskyApi;
protected PushNotificationClient pushNotificationClient;
protected Dispatcher mastodonApiDispatcher;
protected PendingIntent serviceAlarmIntent;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@ -69,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
@ -161,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() {
@ -203,49 +207,66 @@ public class BaseActivity extends AppCompatActivity {
}
protected void enablePushNotifications() {
if (BuildConfig.USES_PUSH_NOTIFICATIONS) {
String token = MessagingService.getInstanceToken();
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);
}
}

View file

@ -307,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";

View file

@ -15,7 +15,6 @@
package com.keylesspalace.tusky;
import android.app.NotificationManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
@ -215,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 */

View file

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

View file

@ -15,17 +15,16 @@
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);
}

View file

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