Merge branch 'master' into #136

This commit is contained in:
torrentcome 2017-05-29 12:14:09 +02:00 committed by torrentcome
commit 2b9f19805f
39 changed files with 525 additions and 648 deletions

1
app/.gitignore vendored
View File

@ -1,3 +1,2 @@
/build /build
app-release.apk app-release.apk
app-google-release.apk

View File

@ -7,19 +7,11 @@ android {
applicationId "com.keylesspalace.tusky" applicationId "com.keylesspalace.tusky"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 25 targetSdkVersion 25
versionCode 16 versionCode 17
versionName "1.1.3" versionName "1.1.4-beta.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary true vectorDrawables.useSupportLibrary true
} }
productFlavors {
google {
buildConfigField "boolean", "USES_PUSH_NOTIFICATIONS", "true"
}
fdroid {
buildConfigField "boolean", "USES_PUSH_NOTIFICATIONS", "false"
}
}
buildTypes { buildTypes {
release { release {
minifyEnabled true minifyEnabled true
@ -53,17 +45,16 @@ dependencies {
compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0' compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'
compile 'com.pkmmte.view:circularimageview:1.1' compile 'com.pkmmte.view:circularimageview:1.1'
compile 'com.github.varunest:sparkbutton:1.0.5' compile 'com.github.varunest:sparkbutton:1.0.5'
compile 'com.mikhaellopez:circularfillableloaders:1.2.0'
compile 'com.github.chrisbanes:PhotoView:2.0.0' compile 'com.github.chrisbanes:PhotoView:2.0.0'
compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar'
compile 'com.github.arimorty:floatingsearchview:2.0.4' compile 'com.github.arimorty:floatingsearchview:2.0.4'
compile 'com.theartofdev.edmodo:android-image-cropper:2.4.3' compile 'com.theartofdev.edmodo:android-image-cropper:2.4.3'
compile 'com.jakewharton:butterknife:8.5.1' compile 'com.jakewharton:butterknife:8.5.1'
compile 'org.jsoup:jsoup:1.10.2' compile 'org.jsoup:jsoup:1.10.2'
googleCompile 'com.google.firebase:firebase-messaging:10.2.4' compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
googleCompile 'com.google.firebase:firebase-crash:10.2.4' compile('org.eclipse.paho:org.eclipse.paho.android.service:1.1.1') {
exclude module: 'support-v4'
}
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1' annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
} }
apply plugin: 'com.google.gms.google-services'

View File

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

View File

@ -46,6 +46,7 @@
# remove all logging from production apk # remove all logging from production apk
-assumenosideeffects class android.util.Log { -assumenosideeffects class android.util.Log {
public static *** getStackTraceString(...);
public static *** d(...); public static *** d(...);
public static *** w(...); public static *** w(...);
public static *** v(...); public static *** v(...);

View File

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

View File

@ -1,142 +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 com.google.gson.Gson;
import com.google.gson.GsonBuilder;
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 com.keylesspalace.tusky.network.MastodonAPI;
import com.keylesspalace.tusky.util.NotificationMaker;
import com.keylesspalace.tusky.util.OkHttpUtils;
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();
}
public static String getInstanceToken() {
// This is only used for the "google" build flavor, so this version is just a stub method.
return null;
}
}

View File

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

View File

@ -1,133 +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.iid.FirebaseInstanceId;
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.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.util.Log;
import com.keylesspalace.tusky.util.NotificationMaker;
import com.keylesspalace.tusky.util.OkHttpUtils;
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);
}
public static String getInstanceToken() {
return FirebaseInstanceId.getInstance().getToken();
}
}

View File

@ -1,86 +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 com.keylesspalace.tusky.network.TuskyAPI;
import com.keylesspalace.tusky.util.Log;
import com.keylesspalace.tusky.util.OkHttpUtils;
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());
}
});
}
}
}

View File

@ -6,15 +6,17 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_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 <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:name=".TuskyApplication"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme">
android:name=".TuskyApplication">
<activity <activity
android:name=".SplashActivity" android:name=".SplashActivity"
@ -83,6 +85,7 @@
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" /> <receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
<service android:name="org.eclipse.paho.android.service.MqttService" />
<service <service
tools:targetApi="24" tools:targetApi="24"
android:name="com.keylesspalace.tusky.service.TuskyTileService" android:name="com.keylesspalace.tusky.service.TuskyTileService"

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
@ -37,6 +38,7 @@ import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -49,10 +51,9 @@ import com.keylesspalace.tusky.fragment.SFragment;
import com.keylesspalace.tusky.interfaces.LinkListener; import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.interfaces.StatusRemoveListener; import com.keylesspalace.tusky.interfaces.StatusRemoveListener;
import com.keylesspalace.tusky.pager.AccountPagerAdapter; import com.keylesspalace.tusky.pager.AccountPagerAdapter;
import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.Assert; import com.keylesspalace.tusky.util.Assert;
import com.keylesspalace.tusky.util.Log;
import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.pkmmte.view.CircularImageView; import com.pkmmte.view.CircularImageView;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
@ -136,23 +137,17 @@ public class AccountActivity extends BaseActivity implements SFragment.OnUserRem
@AttrRes int attribute; @AttrRes int attribute;
if (collapsingToolbar.getHeight() + verticalOffset if (collapsingToolbar.getHeight() + verticalOffset
< 2 * ViewCompat.getMinimumHeight(collapsingToolbar)) { < 2 * ViewCompat.getMinimumHeight(collapsingToolbar)) {
if (getSupportActionBar() != null && loadedAccount != null) {
getSupportActionBar().setTitle(loadedAccount.getDisplayName());
toolbar.setTitleTextColor(ThemeUtils.getColor(AccountActivity.this, toolbar.setTitleTextColor(ThemeUtils.getColor(AccountActivity.this,
android.R.attr.textColorPrimary)); android.R.attr.textColorPrimary));
String subtitle = String.format(getString(R.string.status_username_format),
loadedAccount.username);
getSupportActionBar().setSubtitle(subtitle);
toolbar.setSubtitleTextColor(ThemeUtils.getColor(AccountActivity.this, toolbar.setSubtitleTextColor(ThemeUtils.getColor(AccountActivity.this,
android.R.attr.textColorSecondary)); android.R.attr.textColorSecondary));
}
attribute = R.attr.account_toolbar_icon_tint_collapsed; attribute = R.attr.account_toolbar_icon_tint_collapsed;
} else { } else {
if (getSupportActionBar() != null) { toolbar.setTitleTextColor(Color.TRANSPARENT);
getSupportActionBar().setTitle(""); toolbar.setSubtitleTextColor(Color.TRANSPARENT);
getSupportActionBar().setSubtitle("");
}
attribute = R.attr.account_toolbar_icon_tint_uncollapsed; attribute = R.attr.account_toolbar_icon_tint_uncollapsed;
} }
if (attribute != priorAttribute) { if (attribute != priorAttribute) {
@ -245,6 +240,15 @@ public class AccountActivity extends BaseActivity implements SFragment.OnUserRem
displayName.setText(account.getDisplayName()); displayName.setText(account.getDisplayName());
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(account.getDisplayName());
String subtitle = String.format(getString(R.string.status_username_format),
account.username);
getSupportActionBar().setSubtitle(subtitle);
}
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(this) boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean("customTabs", true); .getBoolean("customTabs", true);
LinkHelper.setClickableText(note, account.note, null, useCustomTabs, new LinkListener() { LinkHelper.setClickableText(note, account.note, null, useCustomTabs, new LinkListener() {

View File

@ -15,8 +15,6 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -24,23 +22,24 @@ import android.graphics.Color;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.text.Spanned; import android.text.Spanned;
import android.util.Log;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.Menu; import android.view.Menu;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.keylesspalace.tusky.entity.Session;
import com.keylesspalace.tusky.json.SpannedTypeAdapter; import com.keylesspalace.tusky.json.SpannedTypeAdapter;
import com.keylesspalace.tusky.json.StringWithEmoji; import com.keylesspalace.tusky.json.StringWithEmoji;
import com.keylesspalace.tusky.json.StringWithEmojiTypeAdapter; import com.keylesspalace.tusky.json.StringWithEmojiTypeAdapter;
import com.keylesspalace.tusky.network.MastodonAPI; import com.keylesspalace.tusky.network.MastodonAPI;
import com.keylesspalace.tusky.network.TuskyAPI; import com.keylesspalace.tusky.network.TuskyApi;
import com.keylesspalace.tusky.util.Log;
import com.keylesspalace.tusky.util.OkHttpUtils; import com.keylesspalace.tusky.util.OkHttpUtils;
import com.keylesspalace.tusky.util.PushNotificationClient;
import java.io.IOException; import java.io.IOException;
@ -59,9 +58,9 @@ public class BaseActivity extends AppCompatActivity {
private static final String TAG = "BaseActivity"; // logging tag private static final String TAG = "BaseActivity"; // logging tag
public MastodonAPI mastodonAPI; public MastodonAPI mastodonAPI;
protected TuskyAPI tuskyAPI; public TuskyApi tuskyApi;
protected PushNotificationClient pushNotificationClient;
protected Dispatcher mastodonApiDispatcher; protected Dispatcher mastodonApiDispatcher;
protected PendingIntent serviceAlarmIntent;
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
@ -69,7 +68,8 @@ public class BaseActivity extends AppCompatActivity {
redirectIfNotLoggedIn(); redirectIfNotLoggedIn();
createMastodonAPI(); createMastodonAPI();
createTuskyAPI(); createTuskyApi();
createPushNotificationClient();
/* There isn't presently a way to globally change the theme of a whole application at /* 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 * 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); mastodonAPI = retrofit.create(MastodonAPI.class);
} }
protected void createTuskyAPI() { protected void createTuskyApi() {
if (BuildConfig.USES_PUSH_NOTIFICATIONS) {
Retrofit retrofit = new Retrofit.Builder() Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getString(R.string.tusky_api_url)) .baseUrl("https://" + getString(R.string.tusky_api_url))
.client(OkHttpUtils.getCompatibleClient()) .client(OkHttpUtils.getCompatibleClient())
.addConverterFactory(GsonConverterFactory.create())
.build(); .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() { protected void redirectIfNotLoggedIn() {
@ -203,49 +207,66 @@ public class BaseActivity extends AppCompatActivity {
} }
protected void enablePushNotifications() { protected void enablePushNotifications() {
if (BuildConfig.USES_PUSH_NOTIFICATIONS) { Callback<ResponseBody> callback = new Callback<ResponseBody>() {
String token = MessagingService.getInstanceToken();
tuskyAPI.register(getBaseUrl(), getAccessToken(), token).enqueue(new Callback<ResponseBody>() {
@Override @Override
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) { public void onResponse(Call<ResponseBody> call,
Log.d(TAG, "Enable push notifications response: " + response.message()); retrofit2.Response<ResponseBody> response) {
if (response.isSuccessful()) {
pushNotificationClient.subscribeToTopic(getPushNotificationTopic());
pushNotificationClient.connect(BaseActivity.this);
} else {
onEnablePushNotificationsFailure(response.message());
}
} }
@Override @Override
public void onFailure(Call<ResponseBody> call, Throwable t) { public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.d(TAG, "Enable push notifications failed: " + t.getMessage()); onEnablePushNotificationsFailure(t.getMessage());
} }
}); };
} else { String deviceToken = pushNotificationClient.getDeviceToken();
// Start up the MessagingService on a repeating interval for "pull" notifications. Session session = new Session(getDomain(), getAccessToken(), deviceToken);
long checkInterval = 60 * 1000 * 5; tuskyApi.register(session)
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); .enqueue(callback);
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);
} }
private void onEnablePushNotificationsFailure(String message) {
Log.e(TAG, "Enabling push notifications failed. " + message);
} }
protected void disablePushNotifications() { protected void disablePushNotifications() {
if (BuildConfig.USES_PUSH_NOTIFICATIONS) { Callback<ResponseBody> callback = new Callback<ResponseBody>() {
tuskyAPI.unregister(getBaseUrl(), getAccessToken()).enqueue(new Callback<ResponseBody>() {
@Override @Override
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) { public void onResponse(Call<ResponseBody> call,
Log.d(TAG, "Disable push notifications response: " + response.message()); retrofit2.Response<ResponseBody> response) {
if (response.isSuccessful()) {
pushNotificationClient.unsubscribeToTopic(getPushNotificationTopic());
} else {
onDisablePushNotificationsFailure();
}
} }
@Override @Override
public void onFailure(Call<ResponseBody> call, Throwable t) { public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.d(TAG, "Disable push notifications failed: " + t.getMessage()); onDisablePushNotificationsFailure();
}
});
} else if (serviceAlarmIntent != null) {
// Cancel the repeating call for "pull" notifications.
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.cancel(serviceAlarmIntent);
} }
};
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

@ -56,6 +56,7 @@ import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.util.Log;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
@ -73,7 +74,6 @@ import com.keylesspalace.tusky.fragment.ComposeOptionsFragment;
import com.keylesspalace.tusky.util.CountUpDownLatch; import com.keylesspalace.tusky.util.CountUpDownLatch;
import com.keylesspalace.tusky.util.DownsizeImageTask; import com.keylesspalace.tusky.util.DownsizeImageTask;
import com.keylesspalace.tusky.util.IOUtils; import com.keylesspalace.tusky.util.IOUtils;
import com.keylesspalace.tusky.util.Log;
import com.keylesspalace.tusky.util.MediaUtils; import com.keylesspalace.tusky.util.MediaUtils;
import com.keylesspalace.tusky.util.ParserUtils; import com.keylesspalace.tusky.util.ParserUtils;
import com.keylesspalace.tusky.util.SpanUtils; import com.keylesspalace.tusky.util.SpanUtils;
@ -252,7 +252,9 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
if (replyVisibility != null && startingVisibility != null) { if (replyVisibility != null && startingVisibility != null) {
// Lowest possible visibility setting in response // 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"; startingVisibility = "private";
} else if (startingVisibility.equals("unlisted") || replyVisibility.equals("unlisted")) { } else if (startingVisibility.equals("unlisted") || replyVisibility.equals("unlisted")) {
startingVisibility = "unlisted"; startingVisibility = "unlisted";
@ -637,6 +639,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
try { try {
descriptor = getContentResolver().openAssetFileDescriptor(uri, "r"); descriptor = getContentResolver().openAssetFileDescriptor(uri, "r");
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.d(TAG, Log.getStackTraceString(e));
// Eat this exception, having the descriptor be null is sufficient. // Eat this exception, having the descriptor be null is sufficient.
} }
if (descriptor != null) { if (descriptor != null) {
@ -996,6 +999,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
try { try {
stream = getContentResolver().openInputStream(item.uri); stream = getContentResolver().openInputStream(item.uri);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.d(TAG, Log.getStackTraceString(e));
return; return;
} }

View File

@ -33,6 +33,7 @@ import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.util.Base64; import android.util.Base64;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -44,7 +45,6 @@ import android.widget.ProgressBar;
import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Profile; import com.keylesspalace.tusky.entity.Profile;
import com.keylesspalace.tusky.util.IOUtils; import com.keylesspalace.tusky.util.IOUtils;
import com.keylesspalace.tusky.util.Log;
import com.pkmmte.view.CircularImageView; import com.pkmmte.view.CircularImageView;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
import com.theartofdev.edmodo.cropper.CropImage; import com.theartofdev.edmodo.cropper.CropImage;
@ -486,12 +486,14 @@ public class EditProfileActivity extends BaseActivity {
try { try {
inputStream = contentResolver.openInputStream(uri); inputStream = contentResolver.openInputStream(uri);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.d(TAG, Log.getStackTraceString(e));
return false; return false;
} }
Bitmap sourceBitmap; Bitmap sourceBitmap;
try { try {
sourceBitmap = BitmapFactory.decodeStream(inputStream, null, null); sourceBitmap = BitmapFactory.decodeStream(inputStream, null, null);
} catch (OutOfMemoryError error) { } catch (OutOfMemoryError error) {
Log.d(TAG, Log.getStackTraceString(error));
return false; return false;
} finally { } finally {
IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(inputStream);

View File

@ -29,6 +29,7 @@ import android.support.customtabs.CustomTabsIntent;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
@ -39,7 +40,6 @@ import com.keylesspalace.tusky.entity.AccessToken;
import com.keylesspalace.tusky.entity.AppCredentials; import com.keylesspalace.tusky.entity.AppCredentials;
import com.keylesspalace.tusky.network.MastodonAPI; import com.keylesspalace.tusky.network.MastodonAPI;
import com.keylesspalace.tusky.util.CustomTabsHelper; import com.keylesspalace.tusky.util.CustomTabsHelper;
import com.keylesspalace.tusky.util.Log;
import com.keylesspalace.tusky.util.OkHttpUtils; import com.keylesspalace.tusky.util.OkHttpUtils;
import java.util.HashMap; import java.util.HashMap;
@ -137,7 +137,7 @@ public class LoginActivity extends AppCompatActivity {
s = s.replaceFirst("http://", ""); s = s.replaceFirst("http://", "");
s = s.replaceFirst("https://", ""); s = s.replaceFirst("https://", "");
// If a username was included (e.g. username@example.com), just take what's after the '@'. // If a username was included (e.g. username@example.com), just take what's after the '@'.
int at = s.indexOf('@'); int at = s.lastIndexOf('@');
if (at != -1) { if (at != -1) {
s = s.substring(at + 1); s = s.substring(at + 1);
} }
@ -201,7 +201,7 @@ public class LoginActivity extends AppCompatActivity {
@Override @Override
public void onFailure(Call<AppCredentials> call, Throwable t) { public void onFailure(Call<AppCredentials> call, Throwable t) {
editText.setError(getString(R.string.error_failed_app_registration)); editText.setError(getString(R.string.error_failed_app_registration));
t.printStackTrace(); Log.e(TAG, Log.getStackTraceString(t));
} }
}; };

View File

@ -15,7 +15,6 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.app.NotificationManager;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -37,6 +36,7 @@ import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
@ -48,7 +48,6 @@ import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.fragment.SFragment; import com.keylesspalace.tusky.fragment.SFragment;
import com.keylesspalace.tusky.interfaces.StatusRemoveListener; import com.keylesspalace.tusky.interfaces.StatusRemoveListener;
import com.keylesspalace.tusky.pager.TimelinePagerAdapter; import com.keylesspalace.tusky.pager.TimelinePagerAdapter;
import com.keylesspalace.tusky.util.Log;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.mikepenz.google_material_typeface_library.GoogleMaterial; import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.materialdrawer.AccountHeader; import com.mikepenz.materialdrawer.AccountHeader;
@ -215,8 +214,7 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove
.putString("current", "[]") .putString("current", "[]")
.apply(); .apply();
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))) pushNotificationClient.clearNotifications(this);
.cancel(MessagingService.NOTIFY_ID);
/* After editing a profile, the profile header in the navigation drawer needs to be /* After editing a profile, the profile header in the navigation drawer needs to be
* refreshed */ * refreshed */

View File

@ -25,6 +25,7 @@ import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -33,7 +34,6 @@ import android.widget.EditText;
import com.keylesspalace.tusky.adapter.ReportAdapter; import com.keylesspalace.tusky.adapter.ReportAdapter;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.util.HtmlUtils; import com.keylesspalace.tusky.util.HtmlUtils;
import com.keylesspalace.tusky.util.Log;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import java.util.ArrayList; import java.util.ArrayList;

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

@ -25,6 +25,7 @@ import android.support.design.widget.TabLayout;
import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -42,9 +43,8 @@ import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.network.MastodonAPI; import com.keylesspalace.tusky.network.MastodonAPI;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import com.keylesspalace.tusky.util.Log;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import java.util.List; import java.util.List;

View File

@ -27,6 +27,7 @@ import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -39,9 +40,8 @@ import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.interfaces.StatusRemoveListener; import com.keylesspalace.tusky.interfaces.StatusRemoveListener;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import com.keylesspalace.tusky.util.Log;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import java.util.List; import java.util.List;

View File

@ -28,6 +28,7 @@ import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -38,10 +39,9 @@ import com.keylesspalace.tusky.adapter.TimelineAdapter;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.interfaces.StatusRemoveListener; import com.keylesspalace.tusky.interfaces.StatusRemoveListener;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import com.keylesspalace.tusky.util.Log;
import com.keylesspalace.tusky.receiver.TimelineReceiver; import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;

View File

@ -25,6 +25,7 @@ import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -38,9 +39,8 @@ import com.keylesspalace.tusky.network.MastodonAPI;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.interfaces.StatusRemoveListener; import com.keylesspalace.tusky.interfaces.StatusRemoveListener;
import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
import com.keylesspalace.tusky.util.Log;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;

View File

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

View File

@ -9,6 +9,7 @@ import android.preference.PreferenceManager;
import android.support.customtabs.CustomTabsIntent; import android.support.customtabs.CustomTabsIntent;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.util.Log;
import android.view.View; import android.view.View;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;

View File

@ -23,6 +23,7 @@ import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.media.ExifInterface; import android.support.media.ExifInterface;
import android.util.Log;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -32,6 +33,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
private static final String TAG = "DownsizeImageTask";
private int sizeLimit; private int sizeLimit;
private ContentResolver contentResolver; private ContentResolver contentResolver;
private Listener listener; private Listener listener;
@ -100,6 +102,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
try { try {
inputStream = contentResolver.openInputStream(uri); inputStream = contentResolver.openInputStream(uri);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.d(TAG, Log.getStackTraceString(e));
return ExifInterface.ORIENTATION_UNDEFINED; return ExifInterface.ORIENTATION_UNDEFINED;
} }
if (inputStream == null) { if (inputStream == null) {
@ -109,6 +112,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
try { try {
exifInterface = new ExifInterface(inputStream); exifInterface = new ExifInterface(inputStream);
} catch (IOException e) { } catch (IOException e) {
Log.d(TAG, Log.getStackTraceString(e));
IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(inputStream);
return ExifInterface.ORIENTATION_UNDEFINED; return ExifInterface.ORIENTATION_UNDEFINED;
} }

View File

@ -1,53 +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.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 {
private static final boolean LOGGING_ENABLED = BuildConfig.DEBUG;
public static void i(String tag, String string) {
if (LOGGING_ENABLED) {
android.util.Log.i(tag, string);
}
}
public static void e(String tag, String string) {
if (LOGGING_ENABLED) {
android.util.Log.e(tag, string);
}
}
public static void d(String tag, String string) {
if (LOGGING_ENABLED) {
android.util.Log.d(tag, string);
}
}
public static void v(String tag, String string) {
if (LOGGING_ENABLED) {
android.util.Log.v(tag, string);
}
}
public static void w(String tag, String string) {
if (LOGGING_ENABLED) {
android.util.Log.w(tag, string);
}
}
}

View File

@ -28,6 +28,7 @@ import android.provider.Settings;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder; import android.support.v4.app.TaskStackBuilder;
import android.util.Log;
import com.keylesspalace.tusky.MainActivity; import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
@ -41,6 +42,9 @@ import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
public class NotificationMaker { public class NotificationMaker {
public static final String TAG = "NotificationMaker";
public static void make(final Context context, final int notifyId, Notification body) { public static void make(final Context context, final int notifyId, Notification body) {
final SharedPreferences preferences = final SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(context); PreferenceManager.getDefaultSharedPreferences(context);
@ -68,7 +72,7 @@ public class NotificationMaker {
alreadyContains = true; alreadyContains = true;
} }
} catch (JSONException e) { } catch (JSONException e) {
e.printStackTrace(); Log.d(TAG, Log.getStackTraceString(e));
} }
} }
@ -129,7 +133,7 @@ public class NotificationMaker {
builder.setContentTitle(String.format(context.getString(R.string.notification_title_summary), currentNotifications.length())) builder.setContentTitle(String.format(context.getString(R.string.notification_title_summary), currentNotifications.length()))
.setContentText(truncateWithEllipses(joinNames(context, currentNotifications), 40)); .setContentText(truncateWithEllipses(joinNames(context, currentNotifications), 40));
} catch (JSONException e) { } catch (JSONException e) {
e.printStackTrace(); Log.d(TAG, Log.getStackTraceString(e));
} }
} }

View File

@ -17,6 +17,7 @@ package com.keylesspalace.tusky.util;
import android.os.Build; import android.os.Build;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.Log;
import com.keylesspalace.tusky.BuildConfig; import com.keylesspalace.tusky.BuildConfig;

View File

@ -5,6 +5,7 @@ import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import android.webkit.URLUtil; import android.webkit.URLUtil;
import org.jsoup.Connection; import org.jsoup.Connection;

View File

@ -0,0 +1,238 @@
package com.keylesspalace.tusky.util;
import android.app.NotificationManager;
import android.content.Context;
import android.support.annotation.NonNull;
import android.text.Spanned;
import android.util.Log;
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();
}
}

View File

@ -27,7 +27,6 @@ import com.keylesspalace.tusky.util.Assert;
public class FlowLayout extends ViewGroup { public class FlowLayout extends ViewGroup {
private int paddingHorizontal; // internal padding between child views private int paddingHorizontal; // internal padding between child views
private int paddingVertical; // private int paddingVertical; //
private int totalHeight;
public FlowLayout(Context context, AttributeSet attrs) { public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0); this(context, attrs, 0);
@ -35,11 +34,10 @@ public class FlowLayout extends ViewGroup {
public FlowLayout(Context context, AttributeSet attrs, int defStyle) { public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle); super(context, attrs, defStyle);
TypedArray a = context.getTheme().obtainStyledAttributes( TypedArray a = context.getTheme()
attrs, R.styleable.FlowLayout, 0, 0); .obtainStyledAttributes(attrs, R.styleable.FlowLayout, 0, 0);
try { try {
paddingHorizontal = a.getDimensionPixelSize( paddingHorizontal = a.getDimensionPixelSize(R.styleable.FlowLayout_paddingHorizontal, 0);
R.styleable.FlowLayout_paddingHorizontal, 0);
paddingVertical = a.getDimensionPixelSize(R.styleable.FlowLayout_paddingVertical, 0); paddingVertical = a.getDimensionPixelSize(R.styleable.FlowLayout_paddingVertical, 0);
} finally { } finally {
a.recycle(); a.recycle();
@ -60,26 +58,29 @@ public class FlowLayout extends ViewGroup {
} else { } else {
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
} }
totalHeight = 0; int rowHeight = 0;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
View child = getChildAt(i); View child = getChildAt(i);
if (child.getVisibility() != GONE) { if (child.getVisibility() != GONE) {
child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST);
childHeightMeasureSpec); child.measure(widthSpec, childHeightMeasureSpec);
int childwidth = child.getMeasuredWidth(); int childwidth = child.getMeasuredWidth();
totalHeight = Math.max(totalHeight, child.getMeasuredHeight() + paddingVertical); int childHeight = child.getMeasuredHeight();
if (x + childwidth > width) { if (x + childwidth > width) {
x = getPaddingLeft(); x = getPaddingLeft();
y += totalHeight; y += rowHeight;
rowHeight = childHeight + paddingVertical;
} else {
rowHeight = Math.max(rowHeight, childHeight + paddingVertical);
} }
x += childwidth + paddingHorizontal; x += childwidth + paddingHorizontal;
} }
} }
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) { if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
height = y + totalHeight; height = y + rowHeight;
} else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) { } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
if (y + totalHeight < height) { if (y + rowHeight < height) {
height = y + totalHeight; height = y + rowHeight;
} }
} }
height += 5; // Fudge to avoid clipping bottom of last row. height += 5; // Fudge to avoid clipping bottom of last row.
@ -91,6 +92,7 @@ public class FlowLayout extends ViewGroup {
final int width = r - l; final int width = r - l;
int x = getPaddingLeft(); int x = getPaddingLeft();
int y = getPaddingTop(); int y = getPaddingTop();
int rowHeight = 0;
for (int i = 0; i < getChildCount(); i++) { for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i); View child = getChildAt(i);
if (child.getVisibility() != GONE) { if (child.getVisibility() != GONE) {
@ -98,7 +100,10 @@ public class FlowLayout extends ViewGroup {
int childHeight = child.getMeasuredHeight(); int childHeight = child.getMeasuredHeight();
if (x + childWidth > width) { if (x + childWidth > width) {
x = getPaddingLeft(); x = getPaddingLeft();
y += totalHeight; y += rowHeight;
rowHeight = childHeight + paddingVertical;
} else {
rowHeight = Math.max(childHeight, rowHeight);
} }
child.layout(x, y, x + childWidth, y + childHeight); child.layout(x, y, x + childWidth, y + childHeight);
x += childWidth + paddingHorizontal; x += childWidth + paddingHorizontal;

View File

@ -30,16 +30,12 @@
android:paddingTop="16dp" android:paddingTop="16dp"
android:paddingBottom="16dp"> android:paddingBottom="16dp">
<com.mikhaellopez.circularfillableloaders.CircularFillableLoaders <ImageView
android:id="@+id/circularFillableLoaders" android:id="@+id/logo"
android:layout_width="200dp" android:layout_width="200dp"
android:layout_height="200dp" android:layout_height="200dp"
android:src="@mipmap/ic_logo" android:src="@mipmap/ic_logo"
app:cfl_border="true" android:contentDescription="@null" />
app:cfl_border_width="4dp"
app:cfl_progress="80"
app:cfl_wave_amplitude="0.08"
app:cfl_wave_color="?attr/splash_wave_color" />
<TextView <TextView
android:id="@+id/versionTV" android:id="@+id/versionTV"

Binary file not shown.

View File

@ -29,9 +29,12 @@
<string name="title_followers">Abonnés</string> <string name="title_followers">Abonnés</string>
<string name="title_favourites">Favoris</string> <string name="title_favourites">Favoris</string>
<string name="title_blocks">Utilisateurs bloqués</string> <string name="title_blocks">Utilisateurs bloqués</string>
<string name="title_mutes">Utilisateurs muets</string>
<string name="title_edit_profile">Editer votre profil</string>
<string name="title_follow_requests">Demande de follow</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s boosté</string> <string name="status_boosted_format">%s a boosté</string>
<string name="status_sensitive_media_title">Média sensible</string> <string name="status_sensitive_media_title">Média sensible</string>
<string name="status_sensitive_media_directions">Cliquer pour voir</string> <string name="status_sensitive_media_directions">Cliquer pour voir</string>
<string name="status_content_warning_show_more">Voir plus</string> <string name="status_content_warning_show_more">Voir plus</string>
@ -91,7 +94,12 @@
<string name="action_clear">Nettoyer</string> <string name="action_clear">Nettoyer</string>
<string name="action_save">Sauvegarder</string> <string name="action_save">Sauvegarder</string>
<string name="action_edit_profile">Modifier le profil</string> <string name="action_edit_profile">Modifier le profil</string>
<string name="action_accept">Accepter</string>
<string name="action_logout_confirm">Voulez-vous vous déconnectez ?</string>
<string name="action_reject">Rejeter</string>
<string name="action_undo">Annuler</string>
<string name="action_view_follow_requests">Demandes de follow</string>
<string name="action_view_mutes">Utilisateurs muets</string>
<string name="send_status_link_to">Partager lURL de votre pouet avec…</string> <string name="send_status_link_to">Partager lURL de votre pouet avec…</string>
<string name="send_status_content_to">Partager le pouet avec…</string> <string name="send_status_content_to">Partager le pouet avec…</string>
@ -122,6 +130,7 @@
<string name="dialog_title_finishing_media_upload">Média mis en ligne avec succès.</string> <string name="dialog_title_finishing_media_upload">Média mis en ligne avec succès.</string>
<string name="dialog_message_uploading_media">Mise en ligne…</string> <string name="dialog_message_uploading_media">Mise en ligne…</string>
<string name="dialog_download_image">Télécharger</string> <string name="dialog_download_image">Télécharger</string>
<string name="dialog_message_follow_request">Demande de follow en attente d\'une réponse</string>
<string name="visibility_public">Public : afficher dans les fils publics.</string> <string name="visibility_public">Public : afficher dans les fils publics.</string>
<string name="visibility_unlisted">Non listé : ne pas afficher dans les fils publics.</string> <string name="visibility_unlisted">Non listé : ne pas afficher dans les fils publics.</string>
@ -145,6 +154,10 @@
<string name="pref_title_browser_settings">Navigateur</string> <string name="pref_title_browser_settings">Navigateur</string>
<string name="pref_title_custom_tabs">Utiliser le navigateur intégré.</string> <string name="pref_title_custom_tabs">Utiliser le navigateur intégré.</string>
<string name="pref_title_hide_follow_button">Masquer le bouton de suivi lors du défilement.</string> <string name="pref_title_hide_follow_button">Masquer le bouton de suivi lors du défilement.</string>
<string name="pref_title_show_boosts">Montrer les boosts</string>
<string name="pref_title_show_replies">Montrer les réponses</string>
<string name="pref_title_status_tabs">Onglets</string>
<string name="pref_title_status_filter">Filtrage de fil</string>
<string name="notification_mention_format">%s a mentionné votre nom.</string> <string name="notification_mention_format">%s a mentionné votre nom.</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s et %4$d plus</string> <string name="notification_summary_large">%1$s, %2$s, %3$s et %4$d plus</string>
@ -155,4 +168,15 @@
<string name="description_account_locked">Compte bloqué.</string> <string name="description_account_locked">Compte bloqué.</string>
<string name="status_share_content">Partager le contenu du pouet.</string> <string name="status_share_content">Partager le contenu du pouet.</string>
<string name="status_share_link">Partager le lien du pouet.</string> <string name="status_share_link">Partager le lien du pouet.</string>
<string name="about_tusky_account">Compte officiel de Tusky</string>
<string name="about_title_activity">À propos</string>
<string name="about_project_site">Site du projet : https://tusky.keylesspalace.com</string>
<string name="about_bug_feature_request_site">Rapport de bugs &amp; demande de fonctionnalités : https://github.com/Vavassor/Tusky/issues</string>
<string name="about_application_version">Version : %s</string>
<string name="state_follow_requested">Demande effectuée</string>
<string name="confirmation_unblocked">Utilisateur débloqué</string>
</resources> </resources>

View File

@ -31,6 +31,8 @@
<string name="title_favourites">お気に入り</string> <string name="title_favourites">お気に入り</string>
<string name="title_mutes">ミュートしたユーザー</string> <string name="title_mutes">ミュートしたユーザー</string>
<string name="title_blocks">ブロックしたユーザー</string> <string name="title_blocks">ブロックしたユーザー</string>
<string name="title_follow_requests">フォローリクエスト</string>
<string name="title_edit_profile">プロフィールを編集</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%sさんがブーストしました</string> <string name="status_boosted_format">%sさんがブーストしました</string>
@ -58,6 +60,7 @@
<string name="action_compose">新規投稿</string> <string name="action_compose">新規投稿</string>
<string name="action_login">Mastodonでログイン</string> <string name="action_login">Mastodonでログイン</string>
<string name="action_logout">ログアウト</string> <string name="action_logout">ログアウト</string>
<string name="action_logout_confirm">ログアウトしますか?</string>
<string name="action_follow">フォローする</string> <string name="action_follow">フォローする</string>
<string name="action_unfollow">フォロー解除</string> <string name="action_unfollow">フォロー解除</string>
<string name="action_block">ブロック</string> <string name="action_block">ブロック</string>
@ -78,6 +81,7 @@
<string name="action_view_favourites">お気に入り</string> <string name="action_view_favourites">お気に入り</string>
<string name="action_view_mutes">ミュートしたユーザー</string> <string name="action_view_mutes">ミュートしたユーザー</string>
<string name="action_view_blocks">ブロックしたユーザー</string> <string name="action_view_blocks">ブロックしたユーザー</string>
<string name="action_view_follow_requests">フォローリクエスト</string>
<string name="action_view_thread">スレッド</string> <string name="action_view_thread">スレッド</string>
<string name="action_view_media">メディア</string> <string name="action_view_media">メディア</string>
<string name="action_open_in_web">ブラウザで開く</string> <string name="action_open_in_web">ブラウザで開く</string>
@ -95,6 +99,8 @@
<string name="action_save">保存</string> <string name="action_save">保存</string>
<string name="action_edit_profile">プロフィールを編集</string> <string name="action_edit_profile">プロフィールを編集</string>
<string name="action_undo">取り消す</string> <string name="action_undo">取り消す</string>
<string name="action_accept">許可</string>
<string name="action_reject">拒否</string>
<string name="send_status_link_to">トゥートのURLを共有…</string> <string name="send_status_link_to">トゥートのURLを共有…</string>
<string name="send_status_content_to">トゥートを共有…</string> <string name="send_status_content_to">トゥートを共有…</string>
@ -117,6 +123,8 @@
<string name="link_whats_an_instance">インスタンスとは?</string> <string name="link_whats_an_instance">インスタンスとは?</string>
<string name="login_connection">接続中…</string>
<string name="dialog_whats_an_instance">mastodon.social, mstdn.jp, pawoo.net や <string name="dialog_whats_an_instance">mastodon.social, mstdn.jp, pawoo.net や
<a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md">その他</a> <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md">その他</a>
のような、あらゆるインスタンスのアドレスやドメインを入力できます。 のような、あらゆるインスタンスのアドレスやドメインを入力できます。
@ -128,10 +136,11 @@
<string name="dialog_title_finishing_media_upload">メディアをアップロードしています</string> <string name="dialog_title_finishing_media_upload">メディアをアップロードしています</string>
<string name="dialog_message_uploading_media">アップロード中…</string> <string name="dialog_message_uploading_media">アップロード中…</string>
<string name="dialog_download_image">ダウンロード</string> <string name="dialog_download_image">ダウンロード</string>
<string name="dialog_message_follow_request">フォローリクエストの承認待ち:返答を待っています</string>
<string name="visibility_public">公開:公開タイムラインに投稿する</string> <string name="visibility_public">公開:公開タイムラインに投稿する</string>
<string name="visibility_unlisted">未収載:公開タイムラインには表示しない</string> <string name="visibility_unlisted">未収載:公開タイムラインには表示しない</string>
<string name="visibility_private">非公開:フォロワーだけに公開</string> <string name="visibility_private">フォロワー限定:フォロワーだけに公開</string>
<string name="visibility_direct">ダイレクト:返信先のユーザーだけに公開</string> <string name="visibility_direct">ダイレクト:返信先のユーザーだけに公開</string>
<string name="pref_title_notification_settings">通知</string> <string name="pref_title_notification_settings">通知</string>
@ -151,6 +160,10 @@
<string name="pref_title_browser_settings">ブラウザ</string> <string name="pref_title_browser_settings">ブラウザ</string>
<string name="pref_title_custom_tabs">Chrome Custom Tabsを使用する</string> <string name="pref_title_custom_tabs">Chrome Custom Tabsを使用する</string>
<string name="pref_title_hide_follow_button">スクロール中はフォローボタンを隠す</string> <string name="pref_title_hide_follow_button">スクロール中はフォローボタンを隠す</string>
<string name="pref_title_status_filter">タイムラインのフィルタリング</string>
<string name="pref_title_status_tabs">タブ</string>
<string name="pref_title_show_boosts">ブーストを表示</string>
<string name="pref_title_show_replies">返信を表示</string>
<string name="notification_mention_format">%sさんが返信しました</string> <string name="notification_mention_format">%sさんが返信しました</string>
<string name="notification_summary_large">%1$sさん、%2$sさん、%3$sさんと他%4$d人</string> <string name="notification_summary_large">%1$sさん、%2$sさん、%3$sさんと他%4$d人</string>
@ -159,6 +172,21 @@
<string name="notification_title_summary">%d件の新しい通知</string> <string name="notification_title_summary">%d件の新しい通知</string>
<string name="description_account_locked">非公開アカウント</string> <string name="description_account_locked">非公開アカウント</string>
<string name="about_title_activity">このアプリについて</string>
<!--<string name="about_application_version">アプリのバージョン:%s</string>-->
<!--<string name="about_project_site">
プロジェクトのWebサイト\n
https://tusky.keylesspalace.com
</string>-->
<!--<string name="about_bug_feature_request_site">
バグ報告 &amp; 機能リクエスト:\n
https://github.com/Vavassor/Tusky/issues
</string>-->
<!--<string name="about_tusky_account">Tusky\'s Profile</string>-->
<string name="status_share_content">トゥートの内容を共有</string> <string name="status_share_content">トゥートの内容を共有</string>
<string name="status_share_link">トゥートへのリンクを共有</string> <string name="status_share_link">トゥートへのリンクを共有</string>
<string name="state_follow_requested">フォローリクエスト中</string>
</resources> </resources>

View File

@ -59,6 +59,7 @@
<string name="action_compose">Napisz</string> <string name="action_compose">Napisz</string>
<string name="action_login">Zaloguj z Mastodon</string> <string name="action_login">Zaloguj z Mastodon</string>
<string name="action_logout">Wyloguj</string> <string name="action_logout">Wyloguj</string>
<string name="action_logout_confirm">Czy chcesz wylogować się?</string>
<string name="action_follow">Obserwuj</string> <string name="action_follow">Obserwuj</string>
<string name="action_unfollow">Przestań obserwować</string> <string name="action_unfollow">Przestań obserwować</string>
<string name="action_block">Zablokuj</string> <string name="action_block">Zablokuj</string>
@ -121,10 +122,16 @@
<string name="link_whats_an_instance">Czym jest instancja?</string> <string name="link_whats_an_instance">Czym jest instancja?</string>
<string name="dialog_whats_an_instance">Wprowadź tu domenę lub adres instancji, np. mastodon.social, icosahedron.website, social.tchncs.de, i <string name="login_connection">Łączenie…</string>
<string name="dialog_whats_an_instance">Domenę lub adres instancji może zostać wprowadzona tutaj,
np. mastodon.social, icosahedron.website, social.tchncs.de, i
<a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md">wiele więcej!</a> <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md">wiele więcej!</a>
\n\nJeżeli nie posiadasz jeszcze konta, wprowadź tu nazwę instancji, na której chcesz się zarejestrowa.\n\nInstancja jest miejscem, na którym znajduje się twoje konto, lecz komunikując się z innymi serwerami, działa tak, jakby były jednym portalem. \n\nJeżeli nie posiadasz jeszcze konta, wprowadź tu nazwę instancji, na której chcesz się zarejestrowa.
\n\nMożesz znaleźć więcej informacji na <a href="https://mastodon.social/about">mastodon.social</a>. Instancja jest miejscem, na którym znajduje się twoje konto,
lecz komunikując się z innymi serwerami, działa tak,
jakby były jednym portalem.
Możesz znaleźć więcej informacji na <a href="https://mastodon.social/about">mastodon.social</a>.
</string> </string>
<string name="dialog_title_finishing_media_upload">Kończenie wysyłania treści</string> <string name="dialog_title_finishing_media_upload">Kończenie wysyłania treści</string>
<string name="dialog_message_uploading_media">Wysyłanie…</string> <string name="dialog_message_uploading_media">Wysyłanie…</string>
@ -151,8 +158,12 @@
<string name="pref_title_appearance_settings">Wygląd</string> <string name="pref_title_appearance_settings">Wygląd</string>
<string name="pref_title_light_theme">Użyj jasnego motywu</string> <string name="pref_title_light_theme">Użyj jasnego motywu</string>
<string name="pref_title_browser_settings">Przeglądarka</string> <string name="pref_title_browser_settings">Przeglądarka</string>
<string name="pref_title_custom_tabs">Używaj kart Chrome</string> <string name="pref_title_custom_tabs">Używaj niestandardowych kart Chrome</string>
<string name="pref_title_hide_follow_button">Ukryj przycisk obserwacji podczas przewijania</string> <string name="pref_title_hide_follow_button">Ukryj przycisk obserwacji podczas przewijania</string>
<string name="pref_title_status_filter">Filtrowanie osi czasu</string>
<string name="pref_title_status_tabs">Karty</string>
<string name="pref_title_show_boosts">Pokazuj podbicia</string>
<string name="pref_title_show_replies">Pokazuj odpowiedzi</string>
<string name="notification_mention_format">%s wspomniał o tobie</string> <string name="notification_mention_format">%s wspomniał o tobie</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s i %4$d innych</string> <string name="notification_summary_large">%1$s, %2$s, %3$s i %4$d innych</string>
@ -161,6 +172,19 @@
<string name="notification_title_summary">%d nowych powiadomień</string> <string name="notification_title_summary">%d nowych powiadomień</string>
<string name="description_account_locked">Konto zablokowane</string> <string name="description_account_locked">Konto zablokowane</string>
  <string name="about_title_activity">O programie</string>
<string name="about_application_version">Wersja aplikacji: %s</string>
<string name="about_project_site">
Strona projektu:\n
https://tusky.keylesspalace.com
</string>
<string name="about_bug_feature_request_site">
Zgłoszenia błędów i propozycje funkcji:\n
https://github.com/Vavassor/Tusky/issues
</string>
<string name="about_tusky_account">Profil Tusky\'ego</string>
<string name="status_share_content">Udostępnij zawartość postu</string> <string name="status_share_content">Udostępnij zawartość postu</string>
<string name="status_share_link">Udostępnij link do postu</string> <string name="status_share_link">Udostępnij link do postu</string>

View File

@ -44,15 +44,15 @@
<color name="button_light">#52a5e0</color> <color name="button_light">#52a5e0</color>
<color name="color_background_light">#f4f4f4</color> <color name="color_background_light">#f4f4f4</color>
<color name="window_background_light">#f4f4f4</color> <color name="window_background_light">#f4f4f4</color>
<color name="edit_text_color_light">#000000</color> <color name="edit_text_color_light">#CC000000</color>
<color name="text_color_primary_light">#000000</color> <color name="text_color_primary_light">#CC000000</color>
<color name="text_color_secondary_light">#3c3c3c</color> <color name="text_color_secondary_light">#3c3c3c</color>
<color name="text_color_tertiary_light">#5f636f</color> <color name="text_color_tertiary_light">#5f636f</color>
<color name="text_color_primary_inverse_light">#ffffff</color> <color name="text_color_primary_inverse_light">#ffffff</color>
<color name="text_color_secondary_inverse_light">#ffffff</color> <color name="text_color_secondary_inverse_light">#ffffff</color>
<color name="text_color_tertiary_inverse_light">#ffffff</color> <color name="text_color_tertiary_inverse_light">#ffffff</color>
<color name="toolbar_background_light">#f6f7f7</color> <color name="toolbar_background_light">#f6f7f7</color>
<color name="toolbar_icon_light">#000000</color> <color name="toolbar_icon_light">#7C000000</color>
<color name="image_button_light">#4f4f4f</color> <color name="image_button_light">#4f4f4f</color>
<color name="status_reblog_button_light">#4f4f4f</color> <color name="status_reblog_button_light">#4f4f4f</color>
<color name="status_reblog_button_marked_light">#56a7e1</color> <color name="status_reblog_button_marked_light">#56a7e1</color>
@ -63,9 +63,9 @@
<color name="media_preview_unloaded_background_light">#cfcfcf</color> <color name="media_preview_unloaded_background_light">#cfcfcf</color>
<color name="status_divider_light">#cfcfcf</color> <color name="status_divider_light">#cfcfcf</color>
<color name="tab_page_margin_light">#9F9F9F</color> <color name="tab_page_margin_light">#9F9F9F</color>
<color name="account_toolbar_icon_collapsed_light">#000000</color> <color name="account_toolbar_icon_collapsed_light">#DE000000</color>
<color name="account_header_background_light">#EFEFEF</color> <color name="account_header_background_light">#EFEFEF</color>
<color name="compose_media_button_light">#000000</color> <color name="compose_media_button_light">#DE000000</color>
<color name="compose_media_button_disabled_light">#8F8F8F</color> <color name="compose_media_button_disabled_light">#8F8F8F</color>
<color name="compose_mention_light">#2F5F6F</color> <color name="compose_mention_light">#2F5F6F</color>
<color name="report_status_background_light">#EFEFEF</color> <color name="report_status_background_light">#EFEFEF</color>

View File

@ -2,7 +2,8 @@
<resources> <resources>
<string name="app_name" translatable="false">Tusky</string> <string name="app_name" translatable="false">Tusky</string>
<string name="app_website" translatable="false">https://tusky.keylesspalace.com</string> <string name="app_website" translatable="false">https://tusky.keylesspalace.com</string>
<string name="tusky_api_url" translatable="false">https://tuskynotifier.keylesspalace.com</string> <string name="tusky_api_url" translatable="false">apitusky.keylesspalace.com</string>
<string name="tusky_api_keystore_password" translatable="false">your_password_here</string>
<string name="oauth_scheme" translatable="false">oauth2redirect</string> <string name="oauth_scheme" translatable="false">oauth2redirect</string>
<string name="oauth_redirect_host" translatable="false">com.keylesspalace.tusky</string> <string name="oauth_redirect_host" translatable="false">com.keylesspalace.tusky</string>

View File

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