Multi account feature (#490)
* basic implementation * improve LoginActivity * darken drawer background image * add current avatar in ComposeActivity * add account name to logout dialog * multi account support for notifications * multi account support for notifications * bugfixes & cleanup * fix bug where somethings notifications would open with the wrong user * correctly set active account in SFragment * small improvements
This commit is contained in:
parent
c9004f1d54
commit
92ae463b38
40 changed files with 1293 additions and 773 deletions
|
@ -12,6 +12,7 @@ Tusky is a beautiful Android client for [Mastodon](https://github.com/tootsuite/
|
||||||
|
|
||||||
- Material Design
|
- Material Design
|
||||||
- Most Mastodon APIs implemented
|
- Most Mastodon APIs implemented
|
||||||
|
- Muti-Account support
|
||||||
- completely Open-source - no non-free dependencies like Google services
|
- completely Open-source - no non-free dependencies like Google services
|
||||||
|
|
||||||
#### Head of development
|
#### Head of development
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 27
|
compileSdkVersion 27
|
||||||
|
@ -49,6 +50,7 @@ dependencies {
|
||||||
implementation('com.mikepenz:materialdrawer:6.0.4@aar') {
|
implementation('com.mikepenz:materialdrawer:6.0.4@aar') {
|
||||||
transitive = true
|
transitive = true
|
||||||
}
|
}
|
||||||
|
debugCompile 'im.dino:dbinspector:3.4.1@aar'
|
||||||
implementation "com.android.support:appcompat-v7:$supportLibraryVersion"
|
implementation "com.android.support:appcompat-v7:$supportLibraryVersion"
|
||||||
implementation "com.android.support:customtabs:$supportLibraryVersion"
|
implementation "com.android.support:customtabs:$supportLibraryVersion"
|
||||||
implementation "com.android.support:recyclerview-v7:$supportLibraryVersion"
|
implementation "com.android.support:recyclerview-v7:$supportLibraryVersion"
|
||||||
|
@ -72,6 +74,9 @@ dependencies {
|
||||||
implementation 'android.arch.persistence.room:runtime:1.0.0'
|
implementation 'android.arch.persistence.room:runtime:1.0.0'
|
||||||
kapt 'android.arch.persistence.room:compiler:1.0.0'
|
kapt 'android.arch.persistence.room:compiler:1.0.0'
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
|
|
||||||
|
debugImplementation 'im.dino:dbinspector:3.4.1@aar'
|
||||||
|
|
||||||
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
|
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
|
||||||
exclude group: 'com.android.support', module: 'support-annotations'
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
<uses-permission android:name="android.permission.VIBRATE" /> <!--For notifications-->
|
<uses-permission android:name="android.permission.VIBRATE" /> <!--For notifications-->
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- for day/night mode -->
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- for day/night mode -->
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
|
|
@ -33,6 +33,7 @@ import com.evernote.android.job.JobManager;
|
||||||
import com.evernote.android.job.JobRequest;
|
import com.evernote.android.job.JobRequest;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter;
|
import com.keylesspalace.tusky.json.SpannedTypeAdapter;
|
||||||
import com.keylesspalace.tusky.network.AuthInterceptor;
|
import com.keylesspalace.tusky.network.AuthInterceptor;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
|
@ -123,19 +124,13 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||||
return getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
return getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getAccessToken() {
|
|
||||||
SharedPreferences preferences = getPrivatePreferences();
|
|
||||||
return preferences.getString("accessToken", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean arePushNotificationsEnabled() {
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
return preferences.getBoolean("notificationsEnabled", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String getBaseUrl() {
|
protected String getBaseUrl() {
|
||||||
SharedPreferences preferences = getPrivatePreferences();
|
AccountEntity account = TuskyApplication.getAccountManager().getActiveAccount();
|
||||||
return "https://" + preferences.getString("domain", null);
|
if(account != null) {
|
||||||
|
return "https://" + account.getDomain();
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void createMastodonApi() {
|
protected void createMastodonApi() {
|
||||||
|
@ -149,7 +144,7 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||||
|
|
||||||
OkHttpClient.Builder okBuilder =
|
OkHttpClient.Builder okBuilder =
|
||||||
OkHttpUtils.getCompatibleClientBuilder(preferences)
|
OkHttpUtils.getCompatibleClientBuilder(preferences)
|
||||||
.addInterceptor(new AuthInterceptor(this))
|
.addInterceptor(new AuthInterceptor())
|
||||||
.dispatcher(mastodonApiDispatcher);
|
.dispatcher(mastodonApiDispatcher);
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
|
@ -166,10 +161,7 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void redirectIfNotLoggedIn() {
|
protected void redirectIfNotLoggedIn() {
|
||||||
SharedPreferences preferences = getPrivatePreferences();
|
if (TuskyApplication.getAccountManager().getActiveAccount() == null) {
|
||||||
String domain = preferences.getString("domain", null);
|
|
||||||
String accessToken = preferences.getString("accessToken", null);
|
|
||||||
if (domain == null || accessToken == null) {
|
|
||||||
Intent intent = new Intent(this, LoginActivity.class);
|
Intent intent = new Intent(this, LoginActivity.class);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
|
|
|
@ -78,6 +78,7 @@ import android.widget.Toast;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.keylesspalace.tusky.adapter.MentionAutoCompleteAdapter;
|
import com.keylesspalace.tusky.adapter.MentionAutoCompleteAdapter;
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.TootDao;
|
import com.keylesspalace.tusky.db.TootDao;
|
||||||
import com.keylesspalace.tusky.db.TootEntity;
|
import com.keylesspalace.tusky.db.TootEntity;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
|
@ -96,6 +97,7 @@ import com.keylesspalace.tusky.util.StringUtils;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
import com.keylesspalace.tusky.view.EditTextTyped;
|
import com.keylesspalace.tusky.view.EditTextTyped;
|
||||||
import com.keylesspalace.tusky.view.ProgressImageView;
|
import com.keylesspalace.tusky.view.ProgressImageView;
|
||||||
|
import com.keylesspalace.tusky.view.RoundedTransformation;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
import com.varunest.sparkbutton.helpers.Utils;
|
import com.varunest.sparkbutton.helpers.Utils;
|
||||||
|
|
||||||
|
@ -448,6 +450,23 @@ public final class ComposeActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
|
||||||
|
|
||||||
|
if(activeAccount != null) {
|
||||||
|
|
||||||
|
ImageView composeAvatar = findViewById(R.id.composeAvatar);
|
||||||
|
|
||||||
|
Picasso.with(this).load(activeAccount.getProfilePictureUrl())
|
||||||
|
.transform(new RoundedTransformation(7, 0))
|
||||||
|
.error(R.drawable.avatar_default)
|
||||||
|
.placeholder(R.drawable.avatar_default)
|
||||||
|
.into(composeAvatar);
|
||||||
|
|
||||||
|
composeAvatar.setContentDescription(
|
||||||
|
getString(R.string.compose_active_account_description,
|
||||||
|
activeAccount.getFullName()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,406 +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.AlertDialog;
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.customtabs.CustomTabsIntent;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.entity.AccessToken;
|
|
||||||
import com.keylesspalace.tusky.entity.AppCredentials;
|
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
|
||||||
import com.keylesspalace.tusky.util.CustomTabsHelper;
|
|
||||||
import com.keylesspalace.tusky.util.NotificationManager;
|
|
||||||
import com.keylesspalace.tusky.util.OkHttpUtils;
|
|
||||||
import com.keylesspalace.tusky.util.ResourcesUtils;
|
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import retrofit2.Call;
|
|
||||||
import retrofit2.Callback;
|
|
||||||
import retrofit2.Response;
|
|
||||||
import retrofit2.Retrofit;
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory;
|
|
||||||
|
|
||||||
public class LoginActivity extends AppCompatActivity {
|
|
||||||
private static final String TAG = "LoginActivity"; // logging tag
|
|
||||||
private static String OAUTH_SCOPES = "read write follow";
|
|
||||||
|
|
||||||
private LinearLayout input;
|
|
||||||
private LinearLayout loading;
|
|
||||||
private EditText editText;
|
|
||||||
private SharedPreferences preferences;
|
|
||||||
private String domain;
|
|
||||||
private String clientId;
|
|
||||||
private String clientSecret;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
String[] themeFlavorPair = preferences.getString("appTheme", TuskyApplication.APP_THEME_DEFAULT).split(":");
|
|
||||||
String appTheme = themeFlavorPair[0], themeFlavorPreference = themeFlavorPair[2];
|
|
||||||
|
|
||||||
setTheme(ResourcesUtils.getResourceIdentifier(this, "style", appTheme));
|
|
||||||
|
|
||||||
String flavor = preferences.getString("appThemeFlavor", ThemeUtils.THEME_FLAVOR_DEFAULT);
|
|
||||||
if (flavor.equals(ThemeUtils.THEME_FLAVOR_DEFAULT))
|
|
||||||
flavor = themeFlavorPreference;
|
|
||||||
ThemeUtils.setAppNightMode(flavor);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_login);
|
|
||||||
|
|
||||||
input = findViewById(R.id.login_input);
|
|
||||||
loading = findViewById(R.id.login_loading);
|
|
||||||
editText = findViewById(R.id.edit_text_domain);
|
|
||||||
Button button = findViewById(R.id.button_login);
|
|
||||||
TextView whatsAnInstance = findViewById(R.id.whats_an_instance);
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
domain = savedInstanceState.getString("domain");
|
|
||||||
clientId = savedInstanceState.getString("clientId");
|
|
||||||
clientSecret = savedInstanceState.getString("clientSecret");
|
|
||||||
} else {
|
|
||||||
domain = null;
|
|
||||||
clientId = null;
|
|
||||||
clientSecret = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
preferences = getSharedPreferences(
|
|
||||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
|
||||||
|
|
||||||
button.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
onButtonClick(editText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final Context context = this;
|
|
||||||
|
|
||||||
whatsAnInstance.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
|
||||||
.setMessage(R.string.dialog_whats_an_instance)
|
|
||||||
.setPositiveButton(R.string.action_close,
|
|
||||||
new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
dialog.dismiss();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.show();
|
|
||||||
TextView textView = dialog.findViewById(android.R.id.message);
|
|
||||||
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
|
||||||
outState.putString("domain", domain);
|
|
||||||
outState.putString("clientId", clientId);
|
|
||||||
outState.putString("clientSecret", clientSecret);
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Make sure the user-entered text is just a fully-qualified domain name. */
|
|
||||||
@NonNull
|
|
||||||
private static String validateDomain(String s) {
|
|
||||||
// Strip any schemes out.
|
|
||||||
s = s.replaceFirst("http://", "");
|
|
||||||
s = s.replaceFirst("https://", "");
|
|
||||||
// If a username was included (e.g. username@example.com), just take what's after the '@'.
|
|
||||||
int at = s.lastIndexOf('@');
|
|
||||||
if (at != -1) {
|
|
||||||
s = s.substring(at + 1);
|
|
||||||
}
|
|
||||||
return s.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getOauthRedirectUri() {
|
|
||||||
String scheme = getString(R.string.oauth_scheme);
|
|
||||||
String host = BuildConfig.APPLICATION_ID;
|
|
||||||
return scheme + "://" + host + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
private MastodonApi getApiFor(String domain) {
|
|
||||||
Retrofit retrofit = new Retrofit.Builder()
|
|
||||||
.baseUrl("https://" + domain)
|
|
||||||
.client(OkHttpUtils.getCompatibleClient(preferences))
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
return retrofit.create(MastodonApi.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtain the oauth client credentials for this app. This is only necessary the first time the
|
|
||||||
* app is run on a given server instance. So, after the first authentication, they are
|
|
||||||
* saved in SharedPreferences and every subsequent run they are simply fetched from there.
|
|
||||||
*/
|
|
||||||
private void onButtonClick(final EditText editText) {
|
|
||||||
domain = validateDomain(editText.getText().toString());
|
|
||||||
/* Attempt to get client credentials from SharedPreferences, and if not present
|
|
||||||
* (such as in the case that the domain has never been accessed before)
|
|
||||||
* authenticate with the server and store the received credentials to use next
|
|
||||||
* time. */
|
|
||||||
String prefClientId = preferences.getString(domain + "/client_id", null);
|
|
||||||
String prefClientSecret = preferences.getString(domain + "/client_secret", null);
|
|
||||||
|
|
||||||
if (prefClientId != null && prefClientSecret != null) {
|
|
||||||
clientId = prefClientId;
|
|
||||||
clientSecret = prefClientSecret;
|
|
||||||
redirectUserToAuthorizeAndLogin(editText);
|
|
||||||
} else {
|
|
||||||
Callback<AppCredentials> callback = new Callback<AppCredentials>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull Call<AppCredentials> call,
|
|
||||||
@NonNull Response<AppCredentials> response) {
|
|
||||||
if (!response.isSuccessful()) {
|
|
||||||
editText.setError(getString(R.string.error_failed_app_registration));
|
|
||||||
Log.e(TAG, "App authentication failed. " + response.message());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
AppCredentials credentials = response.body();
|
|
||||||
clientId = credentials.clientId;
|
|
||||||
clientSecret = credentials.clientSecret;
|
|
||||||
preferences.edit()
|
|
||||||
.putString(domain + "/client_id", clientId)
|
|
||||||
.putString(domain + "/client_secret", clientSecret)
|
|
||||||
.apply();
|
|
||||||
redirectUserToAuthorizeAndLogin(editText);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<AppCredentials> call, @NonNull Throwable t) {
|
|
||||||
editText.setError(getString(R.string.error_failed_app_registration));
|
|
||||||
Log.e(TAG, Log.getStackTraceString(t));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
getApiFor(domain)
|
|
||||||
.authenticateApp(getString(R.string.app_name), getOauthRedirectUri(),
|
|
||||||
OAUTH_SCOPES, getString(R.string.app_website))
|
|
||||||
.enqueue(callback);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
editText.setError(getString(R.string.error_invalid_domain));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chain together the key-value pairs into a query string, for either appending to a URL or
|
|
||||||
* as the content of an HTTP request.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
private static String toQueryString(Map<String, String> parameters) {
|
|
||||||
StringBuilder s = new StringBuilder();
|
|
||||||
String between = "";
|
|
||||||
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
|
||||||
s.append(between);
|
|
||||||
s.append(Uri.encode(entry.getKey()));
|
|
||||||
s.append("=");
|
|
||||||
s.append(Uri.encode(entry.getValue()));
|
|
||||||
between = "&";
|
|
||||||
}
|
|
||||||
return s.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean openInCustomTab(Uri uri, Context context) {
|
|
||||||
int toolbarColor = ThemeUtils.getColorById(context, "custom_tab_toolbar");
|
|
||||||
|
|
||||||
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
|
|
||||||
builder.setToolbarColor(toolbarColor);
|
|
||||||
CustomTabsIntent customTabsIntent = builder.build();
|
|
||||||
try {
|
|
||||||
String packageName = CustomTabsHelper.getPackageNameToUse(context);
|
|
||||||
/* If we cant find a package name, it means theres no browser that supports
|
|
||||||
* Chrome Custom Tabs installed. So, we fallback to the webview */
|
|
||||||
if (packageName == null) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
customTabsIntent.intent.setPackage(packageName);
|
|
||||||
customTabsIntent.launchUrl(context, uri);
|
|
||||||
}
|
|
||||||
} catch (ActivityNotFoundException e) {
|
|
||||||
Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void redirectUserToAuthorizeAndLogin(EditText editText) {
|
|
||||||
/* To authorize this app and log in it's necessary to redirect to the domain given,
|
|
||||||
* activity_login there, and the server will redirect back to the app with its response. */
|
|
||||||
String endpoint = MastodonApi.ENDPOINT_AUTHORIZE;
|
|
||||||
String redirectUri = getOauthRedirectUri();
|
|
||||||
Map<String, String> parameters = new HashMap<>();
|
|
||||||
parameters.put("client_id", clientId);
|
|
||||||
parameters.put("redirect_uri", redirectUri);
|
|
||||||
parameters.put("response_type", "code");
|
|
||||||
parameters.put("scope", OAUTH_SCOPES);
|
|
||||||
String url = "https://" + domain + endpoint + "?" + toQueryString(parameters);
|
|
||||||
Uri uri = Uri.parse(url);
|
|
||||||
if (!openInCustomTab(uri, this)) {
|
|
||||||
Intent viewIntent = new Intent(Intent.ACTION_VIEW, uri);
|
|
||||||
if (viewIntent.resolveActivity(getPackageManager()) != null) {
|
|
||||||
startActivity(viewIntent);
|
|
||||||
} else {
|
|
||||||
editText.setError(getString(R.string.error_no_web_browser_found));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
if (domain != null) {
|
|
||||||
preferences.edit()
|
|
||||||
.putString("domain", domain)
|
|
||||||
.putString("clientId", clientId)
|
|
||||||
.putString("clientSecret", clientSecret)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
/* Check if we are resuming during authorization by seeing if the intent contains the
|
|
||||||
* redirect that was given to the server. If so, its response is here! */
|
|
||||||
Uri uri = getIntent().getData();
|
|
||||||
String redirectUri = getOauthRedirectUri();
|
|
||||||
|
|
||||||
preferences = getSharedPreferences(
|
|
||||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
|
||||||
|
|
||||||
if (preferences.getString("accessToken", null) != null
|
|
||||||
&& preferences.getString("domain", null) != null) {
|
|
||||||
// We are already logged in, go to MainActivity
|
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
|
||||||
startActivity(intent);
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uri != null && uri.toString().startsWith(redirectUri)) {
|
|
||||||
// This should either have returned an authorization code or an error.
|
|
||||||
String code = uri.getQueryParameter("code");
|
|
||||||
String error = uri.getQueryParameter("error");
|
|
||||||
|
|
||||||
if (code != null) {
|
|
||||||
/* During the redirect roundtrip this Activity usually dies, which wipes out the
|
|
||||||
* instance variables, so they have to be recovered from where they were saved in
|
|
||||||
* SharedPreferences. */
|
|
||||||
domain = preferences.getString("domain", null);
|
|
||||||
clientId = preferences.getString("clientId", null);
|
|
||||||
clientSecret = preferences.getString("clientSecret", null);
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
/* Since authorization has succeeded, the final step to log in is to exchange
|
|
||||||
* the authorization code for an access token. */
|
|
||||||
Callback<AccessToken> callback = new Callback<AccessToken>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull Call<AccessToken> call, @NonNull Response<AccessToken> response) {
|
|
||||||
if (response.isSuccessful()) {
|
|
||||||
onLoginSuccess(response.body().accessToken);
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
editText.setError(getString(R.string.error_retrieving_oauth_token));
|
|
||||||
Log.e(TAG, String.format("%s %s",
|
|
||||||
getString(R.string.error_retrieving_oauth_token),
|
|
||||||
response.message()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<AccessToken> call, @NonNull Throwable t) {
|
|
||||||
setLoading(false);
|
|
||||||
editText.setError(getString(R.string.error_retrieving_oauth_token));
|
|
||||||
Log.e(TAG, String.format("%s %s",
|
|
||||||
getString(R.string.error_retrieving_oauth_token),
|
|
||||||
t.getMessage()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getApiFor(domain).fetchOAuthToken(clientId, clientSecret, redirectUri, code,
|
|
||||||
"authorization_code").enqueue(callback);
|
|
||||||
} else if (error != null) {
|
|
||||||
/* Authorization failed. Put the error response where the user can read it and they
|
|
||||||
* can try again. */
|
|
||||||
setLoading(false);
|
|
||||||
editText.setError(getString(R.string.error_authorization_denied));
|
|
||||||
Log.e(TAG, getString(R.string.error_authorization_denied) + error);
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
// This case means a junk response was received somehow.
|
|
||||||
editText.setError(getString(R.string.error_authorization_unknown));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setLoading(boolean loadingState) {
|
|
||||||
if (loadingState) {
|
|
||||||
loading.setVisibility(View.VISIBLE);
|
|
||||||
input.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
loading.setVisibility(View.GONE);
|
|
||||||
input.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onLoginSuccess(String accessToken) {
|
|
||||||
boolean committed = preferences.edit()
|
|
||||||
.putString("domain", domain)
|
|
||||||
.putString("accessToken", accessToken)
|
|
||||||
.commit();
|
|
||||||
if (!committed) {
|
|
||||||
setLoading(false);
|
|
||||||
editText.setError(getString(R.string.error_retrieving_oauth_token));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//create notification channels ahead of time so users can edit the settings
|
|
||||||
NotificationManager.createNotificationChannels(this);
|
|
||||||
|
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
|
||||||
startActivity(intent);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
377
app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt
Normal file
377
app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt
Normal file
|
@ -0,0 +1,377 @@
|
||||||
|
/* 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.AlertDialog
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.preference.PreferenceManager
|
||||||
|
import android.support.customtabs.CustomTabsIntent
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.keylesspalace.tusky.entity.AccessToken
|
||||||
|
import com.keylesspalace.tusky.entity.AppCredentials
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.CustomTabsHelper
|
||||||
|
import com.keylesspalace.tusky.util.OkHttpUtils
|
||||||
|
import com.keylesspalace.tusky.util.ResourcesUtils
|
||||||
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
|
import kotlinx.android.synthetic.main.activity_login.*
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
|
||||||
|
|
||||||
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var preferences: SharedPreferences
|
||||||
|
private var domain: String = ""
|
||||||
|
private var clientId: String? = null
|
||||||
|
private var clientSecret: String? = null
|
||||||
|
|
||||||
|
private val oauthRedirectUri: String
|
||||||
|
get() {
|
||||||
|
val scheme = getString(R.string.oauth_scheme)
|
||||||
|
val host = BuildConfig.APPLICATION_ID
|
||||||
|
return "$scheme://$host/"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
val themeFlavorPair = preferences.getString("appTheme", TuskyApplication.APP_THEME_DEFAULT)!!.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||||
|
val appTheme = themeFlavorPair[0]
|
||||||
|
val themeFlavorPreference = themeFlavorPair[2]
|
||||||
|
|
||||||
|
setTheme(ResourcesUtils.getResourceIdentifier(this, "style", appTheme))
|
||||||
|
|
||||||
|
var flavor = preferences.getString("appThemeFlavor", ThemeUtils.THEME_FLAVOR_DEFAULT)
|
||||||
|
if (flavor == ThemeUtils.THEME_FLAVOR_DEFAULT)
|
||||||
|
flavor = themeFlavorPreference
|
||||||
|
ThemeUtils.setAppNightMode(flavor)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_login)
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
domain = savedInstanceState.getString(DOMAIN)
|
||||||
|
clientId = savedInstanceState.getString(CLIENT_ID)
|
||||||
|
clientSecret = savedInstanceState.getString(CLIENT_SECRET)
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences = getSharedPreferences(
|
||||||
|
getString(R.string.preferences_file_key), Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
loginButton.setOnClickListener { onButtonClick() }
|
||||||
|
|
||||||
|
whatsAnInstanceTextView.setOnClickListener {
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.dialog_whats_an_instance)
|
||||||
|
.setPositiveButton(R.string.action_close, null)
|
||||||
|
.show()
|
||||||
|
val textView = dialog.findViewById<TextView>(android.R.id.message)
|
||||||
|
textView.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isAdditionalLogin()) {
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||||
|
} else {
|
||||||
|
toolbar.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
if(item.itemId == android.R.id.home) {
|
||||||
|
onBackPressed()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putString(DOMAIN, domain)
|
||||||
|
outState.putString(CLIENT_ID, clientId)
|
||||||
|
outState.putString(CLIENT_SECRET, clientSecret)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getApiFor(domain: String): MastodonApi {
|
||||||
|
val retrofit = Retrofit.Builder()
|
||||||
|
.baseUrl("https://" + domain)
|
||||||
|
.client(OkHttpUtils.getCompatibleClient(preferences))
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return retrofit.create(MastodonApi::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain the oauth client credentials for this app. This is only necessary the first time the
|
||||||
|
* app is run on a given server instance. So, after the first authentication, they are
|
||||||
|
* saved in SharedPreferences and every subsequent run they are simply fetched from there.
|
||||||
|
*/
|
||||||
|
private fun onButtonClick() {
|
||||||
|
|
||||||
|
loginButton.isEnabled = false
|
||||||
|
|
||||||
|
domain = validateDomain(domainEditText.text.toString())
|
||||||
|
|
||||||
|
val callback = object : Callback<AppCredentials> {
|
||||||
|
override fun onResponse(call: Call<AppCredentials>,
|
||||||
|
response: Response<AppCredentials>) {
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
loginButton.isEnabled = true
|
||||||
|
domainEditText.error = getString(R.string.error_failed_app_registration)
|
||||||
|
Log.e(TAG, "App authentication failed. " + response.message())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val credentials = response.body()
|
||||||
|
clientId = credentials!!.clientId
|
||||||
|
clientSecret = credentials.clientSecret
|
||||||
|
|
||||||
|
redirectUserToAuthorizeAndLogin(domainEditText)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<AppCredentials>, t: Throwable) {
|
||||||
|
loginButton.isEnabled = true
|
||||||
|
domainEditText.error = getString(R.string.error_failed_app_registration)
|
||||||
|
setLoading(false)
|
||||||
|
Log.e(TAG, Log.getStackTraceString(t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
getApiFor(domain)
|
||||||
|
.authenticateApp(getString(R.string.app_name), oauthRedirectUri,
|
||||||
|
OAUTH_SCOPES, getString(R.string.app_website))
|
||||||
|
.enqueue(callback)
|
||||||
|
setLoading(true)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
setLoading(false)
|
||||||
|
domainEditText.error = getString(R.string.error_invalid_domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun redirectUserToAuthorizeAndLogin(editText: EditText) {
|
||||||
|
/* To authorize this app and log in it's necessary to redirect to the domain given,
|
||||||
|
* activity_login there, and the server will redirect back to the app with its response. */
|
||||||
|
val endpoint = MastodonApi.ENDPOINT_AUTHORIZE
|
||||||
|
val redirectUri = oauthRedirectUri
|
||||||
|
val parameters = HashMap<String, String>()
|
||||||
|
parameters["client_id"] = clientId!!
|
||||||
|
parameters["redirect_uri"] = redirectUri
|
||||||
|
parameters["response_type"] = "code"
|
||||||
|
parameters["scope"] = OAUTH_SCOPES
|
||||||
|
val url = "https://" + domain + endpoint + "?" + toQueryString(parameters)
|
||||||
|
val uri = Uri.parse(url)
|
||||||
|
if (!openInCustomTab(uri, this)) {
|
||||||
|
val viewIntent = Intent(Intent.ACTION_VIEW, uri)
|
||||||
|
if (viewIntent.resolveActivity(packageManager) != null) {
|
||||||
|
startActivity(viewIntent)
|
||||||
|
} else {
|
||||||
|
editText.error = getString(R.string.error_no_web_browser_found)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
preferences.edit()
|
||||||
|
.putString("domain", domain)
|
||||||
|
.putString("clientId", clientId)
|
||||||
|
.putString("clientSecret", clientSecret)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
/* Check if we are resuming during authorization by seeing if the intent contains the
|
||||||
|
* redirect that was given to the server. If so, its response is here! */
|
||||||
|
val uri = intent.data
|
||||||
|
val redirectUri = oauthRedirectUri
|
||||||
|
|
||||||
|
if (uri != null && uri.toString().startsWith(redirectUri)) {
|
||||||
|
// This should either have returned an authorization code or an error.
|
||||||
|
val code = uri.getQueryParameter("code")
|
||||||
|
val error = uri.getQueryParameter("error")
|
||||||
|
|
||||||
|
if (code != null) {
|
||||||
|
/* During the redirect roundtrip this Activity usually dies, which wipes out the
|
||||||
|
* instance variables, so they have to be recovered from where they were saved in
|
||||||
|
* SharedPreferences. */
|
||||||
|
domain = preferences.getString(DOMAIN, null)
|
||||||
|
clientId = preferences.getString(CLIENT_ID, null)
|
||||||
|
clientSecret = preferences.getString(CLIENT_SECRET, null)
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
/* Since authorization has succeeded, the final step to log in is to exchange
|
||||||
|
* the authorization code for an access token. */
|
||||||
|
val callback = object : Callback<AccessToken> {
|
||||||
|
override fun onResponse(call: Call<AccessToken>, response: Response<AccessToken>) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
onLoginSuccess(response.body()!!.accessToken)
|
||||||
|
} else {
|
||||||
|
setLoading(false)
|
||||||
|
domainEditText.error = getString(R.string.error_retrieving_oauth_token)
|
||||||
|
Log.e(TAG, String.format("%s %s",
|
||||||
|
getString(R.string.error_retrieving_oauth_token),
|
||||||
|
response.message()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<AccessToken>, t: Throwable) {
|
||||||
|
setLoading(false)
|
||||||
|
domainEditText.error = getString(R.string.error_retrieving_oauth_token)
|
||||||
|
Log.e(TAG, String.format("%s %s",
|
||||||
|
getString(R.string.error_retrieving_oauth_token),
|
||||||
|
t.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getApiFor(domain).fetchOAuthToken(clientId, clientSecret, redirectUri, code,
|
||||||
|
"authorization_code").enqueue(callback)
|
||||||
|
} else if (error != null) {
|
||||||
|
/* Authorization failed. Put the error response where the user can read it and they
|
||||||
|
* can try again. */
|
||||||
|
setLoading(false)
|
||||||
|
domainEditText.error = getString(R.string.error_authorization_denied)
|
||||||
|
Log.e(TAG, String.format("%s %s",
|
||||||
|
getString(R.string.error_authorization_denied),
|
||||||
|
error))
|
||||||
|
} else {
|
||||||
|
// This case means a junk response was received somehow.
|
||||||
|
setLoading(false)
|
||||||
|
domainEditText.error = getString(R.string.error_authorization_unknown)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// first show or user cancelled login
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(loadingState: Boolean) {
|
||||||
|
if (loadingState) {
|
||||||
|
loginLoadingLayout.visibility = View.VISIBLE
|
||||||
|
loginInputLayout.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
loginLoadingLayout.visibility = View.GONE
|
||||||
|
loginInputLayout.visibility = View.VISIBLE
|
||||||
|
loginButton.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAdditionalLogin() : Boolean {
|
||||||
|
return intent.getBooleanExtra(LOGIN_MODE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLoginSuccess(accessToken: String) {
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
TuskyApplication.getAccountManager().addAccount(accessToken, domain)
|
||||||
|
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LoginActivity" // logging tag
|
||||||
|
private const val OAUTH_SCOPES = "read write follow"
|
||||||
|
private const val LOGIN_MODE = "LOGIN_MODE"
|
||||||
|
private const val DOMAIN = "domain"
|
||||||
|
private const val CLIENT_ID = "clientId"
|
||||||
|
private const val CLIENT_SECRET = "clientSecret"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getIntent(context: Context, mode: Boolean): Intent {
|
||||||
|
val loginIntent = Intent(context, LoginActivity::class.java)
|
||||||
|
loginIntent.putExtra(LOGIN_MODE, mode)
|
||||||
|
return loginIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Make sure the user-entered text is just a fully-qualified domain name. */
|
||||||
|
private fun validateDomain(domain: String): String {
|
||||||
|
// Strip any schemes out.
|
||||||
|
var s = domain.replaceFirst("http://", "")
|
||||||
|
s = s.replaceFirst("https://", "")
|
||||||
|
// If a username was included (e.g. username@example.com), just take what's after the '@'.
|
||||||
|
val at = s.lastIndexOf('@')
|
||||||
|
if (at != -1) {
|
||||||
|
s = s.substring(at + 1)
|
||||||
|
}
|
||||||
|
return s.trim { it <= ' ' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chain together the key-value pairs into a query string, for either appending to a URL or
|
||||||
|
* as the content of an HTTP request.
|
||||||
|
*/
|
||||||
|
private fun toQueryString(parameters: Map<String, String>): String {
|
||||||
|
val s = StringBuilder()
|
||||||
|
var between = ""
|
||||||
|
for ((key, value) in parameters) {
|
||||||
|
s.append(between)
|
||||||
|
s.append(Uri.encode(key))
|
||||||
|
s.append("=")
|
||||||
|
s.append(Uri.encode(value))
|
||||||
|
between = "&"
|
||||||
|
}
|
||||||
|
return s.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openInCustomTab(uri: Uri, context: Context): Boolean {
|
||||||
|
|
||||||
|
val toolbarColor = ThemeUtils.getColorById(context, "custom_tab_toolbar")
|
||||||
|
val builder = CustomTabsIntent.Builder()
|
||||||
|
builder.setToolbarColor(toolbarColor)
|
||||||
|
val customTabsIntent = builder.build()
|
||||||
|
try {
|
||||||
|
val packageName = CustomTabsHelper.getPackageNameToUse(context)
|
||||||
|
/* If we cant find a package name, it means theres no browser that supports
|
||||||
|
* Chrome Custom Tabs installed. So, we fallback to the webview */
|
||||||
|
if (packageName == null) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
customTabsIntent.intent.`package` = packageName
|
||||||
|
customTabsIntent.launchUrl(context, uri)
|
||||||
|
}
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Log.w(TAG, "Activity was not found for intent, " + customTabsIntent.toString())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,10 +33,11 @@ import android.support.v4.view.ViewPager;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||||
import com.keylesspalace.tusky.pager.TimelinePagerAdapter;
|
import com.keylesspalace.tusky.pager.TimelinePagerAdapter;
|
||||||
|
@ -51,6 +52,7 @@ import com.mikepenz.materialdrawer.DrawerBuilder;
|
||||||
import com.mikepenz.materialdrawer.model.DividerDrawerItem;
|
import com.mikepenz.materialdrawer.model.DividerDrawerItem;
|
||||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
|
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
|
||||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem;
|
import com.mikepenz.materialdrawer.model.ProfileDrawerItem;
|
||||||
|
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem;
|
||||||
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
|
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
|
||||||
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
|
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
|
||||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile;
|
import com.mikepenz.materialdrawer.model.interfaces.IProfile;
|
||||||
|
@ -67,6 +69,7 @@ import retrofit2.Response;
|
||||||
|
|
||||||
public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
private static final String TAG = "MainActivity"; // logging tag
|
private static final String TAG = "MainActivity"; // logging tag
|
||||||
|
private static final long DRAWER_ITEM_ADD_ACCOUNT = -13;
|
||||||
private static final long DRAWER_ITEM_EDIT_PROFILE = 0;
|
private static final long DRAWER_ITEM_EDIT_PROFILE = 0;
|
||||||
private static final long DRAWER_ITEM_FAVOURITES = 1;
|
private static final long DRAWER_ITEM_FAVOURITES = 1;
|
||||||
private static final long DRAWER_ITEM_MUTED_USERS = 2;
|
private static final long DRAWER_ITEM_MUTED_USERS = 2;
|
||||||
|
@ -82,14 +85,32 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
private static int COMPOSE_RESULT = 1;
|
private static int COMPOSE_RESULT = 1;
|
||||||
|
|
||||||
private FloatingActionButton composeButton;
|
private FloatingActionButton composeButton;
|
||||||
private String loggedInAccountId;
|
|
||||||
private String loggedInAccountUsername;
|
|
||||||
private AccountHeader headerResult;
|
private AccountHeader headerResult;
|
||||||
private Drawer drawer;
|
private Drawer drawer;
|
||||||
private ViewPager viewPager;
|
private ViewPager viewPager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
||||||
|
// account switching has to be done before MastodonApi is created in super.onCreate
|
||||||
|
Intent intent = getIntent();
|
||||||
|
|
||||||
|
int tabPosition = 0;
|
||||||
|
|
||||||
|
if (intent != null) {
|
||||||
|
long accountId = intent.getLongExtra(NotificationManager.ACCOUNT_ID, -1);
|
||||||
|
|
||||||
|
if(accountId != -1) {
|
||||||
|
// user clicked a notification, show notification tab and switch user if necessary
|
||||||
|
tabPosition = 1;
|
||||||
|
AccountEntity account = TuskyApplication.getAccountManager().getActiveAccount();
|
||||||
|
|
||||||
|
if (account == null || accountId != account.getId()) {
|
||||||
|
TuskyApplication.getAccountManager().setActiveAccount(accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
|
@ -99,8 +120,8 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
viewPager = findViewById(R.id.pager);
|
viewPager = findViewById(R.id.pager);
|
||||||
|
|
||||||
floatingBtn.setOnClickListener(v -> {
|
floatingBtn.setOnClickListener(v -> {
|
||||||
Intent intent = new Intent(getApplicationContext(), ComposeActivity.class);
|
Intent composeIntent = new Intent(getApplicationContext(), ComposeActivity.class);
|
||||||
startActivityForResult(intent, COMPOSE_RESULT);
|
startActivityForResult(composeIntent, COMPOSE_RESULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
setupDrawer();
|
setupDrawer();
|
||||||
|
@ -109,7 +130,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
ThemeUtils.setDrawableTint(this, drawerToggle.getDrawable(), R.attr.toolbar_icon_tint);
|
ThemeUtils.setDrawableTint(this, drawerToggle.getDrawable(), R.attr.toolbar_icon_tint);
|
||||||
drawerToggle.setOnClickListener(v -> drawer.openDrawer());
|
drawerToggle.setOnClickListener(v -> drawer.openDrawer());
|
||||||
|
|
||||||
/* Fetch user info while we're doing other things. This has to be after setting up the
|
/* Fetch user info while we're doing other things. This has to be done after setting up the
|
||||||
* drawer, though, because its callback touches the header in the drawer. */
|
* drawer, though, because its callback touches the header in the drawer. */
|
||||||
fetchUserInfo();
|
fetchUserInfo();
|
||||||
|
|
||||||
|
@ -143,6 +164,15 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
tab.setContentDescription(pageTitles[i]);
|
tab.setContentDescription(pageTitles[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tabPosition != 0) {
|
||||||
|
TabLayout.Tab tab = tabLayout.getTabAt(tabPosition);
|
||||||
|
if (tab != null) {
|
||||||
|
tab.select();
|
||||||
|
} else {
|
||||||
|
tabPosition = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTabSelected(TabLayout.Tab tab) {
|
public void onTabSelected(TabLayout.Tab tab) {
|
||||||
|
@ -151,7 +181,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
tintTab(tab, true);
|
tintTab(tab, true);
|
||||||
|
|
||||||
if(tab.getPosition() == 1) {
|
if(tab.getPosition() == 1) {
|
||||||
NotificationManager.clearNotifications(MainActivity.this);
|
NotificationManager.clearNotificationsForActiveAccount(MainActivity.this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,29 +191,15 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTabReselected(TabLayout.Tab tab) {
|
public void onTabReselected(TabLayout.Tab tab) { }
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Intent intent = getIntent();
|
|
||||||
|
|
||||||
int tabSelected = 0;
|
|
||||||
if (intent != null) {
|
|
||||||
int tabPosition = intent.getIntExtra("tab_position", 0);
|
|
||||||
if (tabPosition != 0) {
|
|
||||||
TabLayout.Tab tab = tabLayout.getTabAt(tabPosition);
|
|
||||||
if (tab != null) {
|
|
||||||
tab.select();
|
|
||||||
tabSelected = tabPosition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
tintTab(tabLayout.getTabAt(i), i == tabSelected);
|
tintTab(tabLayout.getTabAt(i), i == tabPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup push notifications
|
// Setup push notifications
|
||||||
if (arePushNotificationsEnabled()) {
|
if (TuskyApplication.getAccountManager().notificationsEnabled()) {
|
||||||
enablePushNotifications();
|
enablePushNotifications();
|
||||||
} else {
|
} else {
|
||||||
disablePushNotifications();
|
disablePushNotifications();
|
||||||
|
@ -196,7 +212,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
NotificationManager.clearNotifications(this);
|
NotificationManager.clearNotificationsForActiveAccount(this);
|
||||||
|
|
||||||
/* 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 */
|
||||||
|
@ -208,9 +224,6 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(viewPager.getCurrentItem() == 1) {
|
|
||||||
NotificationManager.clearNotifications(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -267,28 +280,18 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
private void setupDrawer() {
|
private void setupDrawer() {
|
||||||
headerResult = new AccountHeaderBuilder()
|
headerResult = new AccountHeaderBuilder()
|
||||||
.withActivity(this)
|
.withActivity(this)
|
||||||
.withSelectionListEnabledForSingleProfile(false)
|
|
||||||
.withDividerBelowHeader(false)
|
.withDividerBelowHeader(false)
|
||||||
.withHeaderBackgroundScaleType(ImageView.ScaleType.CENTER_CROP)
|
.withHeaderBackgroundScaleType(ImageView.ScaleType.CENTER_CROP)
|
||||||
.withOnAccountHeaderProfileImageListener(new AccountHeader.OnAccountHeaderProfileImageListener() {
|
.withCurrentProfileHiddenInList(true)
|
||||||
@Override
|
.withOnAccountHeaderListener((view, profile, current) -> handleProfileClick(profile, current))
|
||||||
public boolean onProfileImageClick(View view, IProfile profile, boolean current) {
|
.addProfiles(
|
||||||
if (current && loggedInAccountId != null) {
|
new ProfileSettingDrawerItem()
|
||||||
Intent intent = new Intent(MainActivity.this, AccountActivity.class);
|
.withIdentifier(DRAWER_ITEM_ADD_ACCOUNT)
|
||||||
intent.putExtra("id", loggedInAccountId);
|
.withName(R.string.add_account_name)
|
||||||
startActivity(intent);
|
.withDescription(R.string.add_account_description)
|
||||||
return true;
|
.withIcon(GoogleMaterial.Icon.gmd_add))
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onProfileImageLongClick(View view, IProfile profile, boolean current) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.withCompactStyle(true)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
headerResult.getView()
|
headerResult.getView()
|
||||||
.findViewById(R.id.material_drawer_account_header_current)
|
.findViewById(R.id.material_drawer_account_header_current)
|
||||||
.setContentDescription(getString(R.string.action_view_profile));
|
.setContentDescription(getString(R.string.action_view_profile));
|
||||||
|
@ -371,6 +374,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
} else if (drawerItemIdentifier == DRAWER_ITEM_LISTS) {
|
} else if (drawerItemIdentifier == DRAWER_ITEM_LISTS) {
|
||||||
startActivity(ListsActivity.newIntent(this));
|
startActivity(ListsActivity.newIntent(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -388,43 +392,78 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean handleProfileClick(IProfile profile, boolean current) {
|
||||||
|
AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
|
||||||
|
|
||||||
|
//open profile when active image was clicked
|
||||||
|
if (current && activeAccount != null) {
|
||||||
|
Intent intent = new Intent(MainActivity.this, AccountActivity.class);
|
||||||
|
intent.putExtra("id", activeAccount.getAccountId());
|
||||||
|
startActivity(intent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
//open LoginActivity to add new account
|
||||||
|
if(profile.getIdentifier() == DRAWER_ITEM_ADD_ACCOUNT ) {
|
||||||
|
startActivity(LoginActivity.getIntent(this, true));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
//change Account
|
||||||
|
changeAccount(profile.getIdentifier());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void changeAccount(long newSelectedId) {
|
||||||
|
TuskyApplication.getAccountManager().setActiveAccount(newSelectedId);
|
||||||
|
|
||||||
|
Intent intent = new Intent(this, MainActivity.class);
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
|
||||||
|
overridePendingTransition(R.anim.explode, R.anim.explode);
|
||||||
|
}
|
||||||
|
|
||||||
private void logout() {
|
private void logout() {
|
||||||
new AlertDialog.Builder(this)
|
|
||||||
.setTitle(R.string.action_logout)
|
|
||||||
.setMessage(R.string.action_logout_confirm)
|
|
||||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
|
||||||
if (arePushNotificationsEnabled()) disablePushNotifications();
|
|
||||||
|
|
||||||
getPrivatePreferences().edit()
|
AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
|
||||||
.remove("domain")
|
|
||||||
.remove("accessToken")
|
|
||||||
.remove("appAccountId")
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
Intent intent = new Intent(MainActivity.this, LoginActivity.class);
|
if(activeAccount != null) {
|
||||||
startActivity(intent);
|
|
||||||
finish();
|
new AlertDialog.Builder(this)
|
||||||
})
|
.setTitle(R.string.action_logout)
|
||||||
.setNegativeButton(android.R.string.no, null)
|
.setMessage(getString(R.string.action_logout_confirm, activeAccount.getFullName()))
|
||||||
.show();
|
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||||
|
|
||||||
|
AccountManager accountManager = TuskyApplication.getAccountManager();
|
||||||
|
|
||||||
|
NotificationManager.deleteNotificationChannelsForAccount(accountManager.getActiveAccount(), MainActivity.this);
|
||||||
|
|
||||||
|
AccountEntity newAccount = accountManager.logActiveAccountOut();
|
||||||
|
|
||||||
|
if (!accountManager.notificationsEnabled()) disablePushNotifications();
|
||||||
|
|
||||||
|
Intent intent;
|
||||||
|
if (newAccount == null) {
|
||||||
|
intent = LoginActivity.getIntent(MainActivity.this, false);
|
||||||
|
} else {
|
||||||
|
intent = new Intent(MainActivity.this, MainActivity.class);
|
||||||
|
}
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
})
|
||||||
|
.setNegativeButton(android.R.string.no, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchUserInfo() {
|
private void fetchUserInfo() {
|
||||||
SharedPreferences preferences = getPrivatePreferences();
|
|
||||||
final String domain = preferences.getString("domain", null);
|
|
||||||
String id = preferences.getString("loggedInAccountId", null);
|
|
||||||
String username = preferences.getString("loggedInAccountUsername", null);
|
|
||||||
|
|
||||||
if (id != null && username != null) {
|
|
||||||
loggedInAccountId = id;
|
|
||||||
loggedInAccountUsername = username;
|
|
||||||
}
|
|
||||||
|
|
||||||
mastodonApi.accountVerifyCredentials().enqueue(new Callback<Account>() {
|
mastodonApi.accountVerifyCredentials().enqueue(new Callback<Account>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<Account> call, @NonNull Response<Account> response) {
|
public void onResponse(@NonNull Call<Account> call, @NonNull Response<Account> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
onFetchUserInfoSuccess(response.body(), domain);
|
onFetchUserInfoSuccess(response.body());
|
||||||
} else {
|
} else {
|
||||||
onFetchUserInfoFailure(new Exception(response.message()));
|
onFetchUserInfoFailure(new Exception(response.message()));
|
||||||
}
|
}
|
||||||
|
@ -437,22 +476,34 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onFetchUserInfoSuccess(Account me, String domain) {
|
private void onFetchUserInfoSuccess(Account me) {
|
||||||
// Add the header image and avatar from the account, into the navigation drawer header.
|
// Add the header image and avatar from the account, into the navigation drawer header.
|
||||||
ImageView background = headerResult.getHeaderBackgroundView();
|
ImageView background = headerResult.getHeaderBackgroundView();
|
||||||
|
background.setColorFilter(ContextCompat.getColor(this, R.color.header_background_filter));
|
||||||
background.setBackgroundColor(ContextCompat.getColor(this, R.color.window_background_dark));
|
background.setBackgroundColor(ContextCompat.getColor(this, R.color.window_background_dark));
|
||||||
Picasso.with(MainActivity.this)
|
Picasso.with(MainActivity.this)
|
||||||
.load(me.header)
|
.load(me.header)
|
||||||
.placeholder(R.drawable.account_header_default)
|
.placeholder(R.drawable.account_header_default)
|
||||||
.into(background);
|
.into(background);
|
||||||
|
|
||||||
headerResult.clear();
|
AccountManager am = TuskyApplication.getAccountManager();
|
||||||
headerResult.addProfiles(
|
|
||||||
new ProfileDrawerItem()
|
am.updateActiveAccount(me);
|
||||||
.withName(me.getDisplayName())
|
|
||||||
.withEmail(String.format("%s@%s", me.username, domain))
|
NotificationManager.createNotificationChannelsForAccount(am.getActiveAccount(), this);
|
||||||
.withIcon(me.avatar)
|
|
||||||
);
|
List<AccountEntity> allAccounts = am.getAllAccountsOrderedByActive();
|
||||||
|
|
||||||
|
for(AccountEntity acc: allAccounts) {
|
||||||
|
headerResult.addProfiles(
|
||||||
|
new ProfileDrawerItem()
|
||||||
|
.withName(acc.getDisplayName())
|
||||||
|
.withIcon(acc.getProfilePictureUrl())
|
||||||
|
.withNameShown(true)
|
||||||
|
.withIdentifier(acc.getId())
|
||||||
|
.withEmail(acc.getFullName()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Show follow requests in the menu, if this is a locked account.
|
// Show follow requests in the menu, if this is a locked account.
|
||||||
if (me.locked) {
|
if (me.locked) {
|
||||||
|
@ -464,14 +515,6 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
|
||||||
drawer.addItemAtPosition(followRequestsItem, 3);
|
drawer.addItemAtPosition(followRequestsItem, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the current login information.
|
|
||||||
loggedInAccountId = me.id;
|
|
||||||
loggedInAccountUsername = me.username;
|
|
||||||
getPrivatePreferences().edit()
|
|
||||||
.putString("loggedInAccountId", loggedInAccountId)
|
|
||||||
.putString("loggedInAccountUsername", loggedInAccountUsername)
|
|
||||||
.putBoolean("loggedInAccountLocked", me.locked)
|
|
||||||
.apply();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onFetchUserInfoFailure(Exception exception) {
|
private void onFetchUserInfoFailure(Exception exception) {
|
||||||
|
|
|
@ -20,23 +20,23 @@ import android.content.SharedPreferences;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import com.evernote.android.job.Job;
|
import com.evernote.android.job.Job;
|
||||||
import com.evernote.android.job.JobCreator;
|
import com.evernote.android.job.JobCreator;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter;
|
import com.keylesspalace.tusky.json.SpannedTypeAdapter;
|
||||||
import com.keylesspalace.tusky.network.AuthInterceptor;
|
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.util.NotificationManager;
|
import com.keylesspalace.tusky.util.NotificationManager;
|
||||||
import com.keylesspalace.tusky.util.OkHttpUtils;
|
import com.keylesspalace.tusky.util.OkHttpUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.math.BigInteger;
|
||||||
import java.util.HashSet;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
@ -49,7 +49,8 @@ import retrofit2.converter.gson.GsonConverterFactory;
|
||||||
|
|
||||||
public final class NotificationPullJobCreator implements JobCreator {
|
public final class NotificationPullJobCreator implements JobCreator {
|
||||||
|
|
||||||
public static final int NOTIFY_ID = 6; // chosen by fair dice roll, guaranteed to be random
|
private static final String TAG = "NotificationPJC";
|
||||||
|
|
||||||
static final String NOTIFICATIONS_JOB_TAG = "notifications_job_tag";
|
static final String NOTIFICATIONS_JOB_TAG = "notifications_job_tag";
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
|
@ -62,15 +63,7 @@ public final class NotificationPullJobCreator implements JobCreator {
|
||||||
@Override
|
@Override
|
||||||
public Job create(@NonNull String tag) {
|
public Job create(@NonNull String tag) {
|
||||||
if (tag.equals(NOTIFICATIONS_JOB_TAG)) {
|
if (tag.equals(NOTIFICATIONS_JOB_TAG)) {
|
||||||
SharedPreferences preferences = context.getSharedPreferences(
|
return new NotificationPullJob(context);
|
||||||
context.getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
|
||||||
final String domain = preferences.getString("domain", null);
|
|
||||||
|
|
||||||
if(domain == null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return new NotificationPullJob(domain, context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -80,7 +73,6 @@ public final class NotificationPullJobCreator implements JobCreator {
|
||||||
context.getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
context.getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||||
|
|
||||||
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder(preferences)
|
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder(preferences)
|
||||||
.addInterceptor(new AuthInterceptor(context))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Gson gson = new GsonBuilder()
|
Gson gson = new GsonBuilder()
|
||||||
|
@ -98,48 +90,72 @@ public final class NotificationPullJobCreator implements JobCreator {
|
||||||
|
|
||||||
private final static class NotificationPullJob extends Job {
|
private final static class NotificationPullJob extends Job {
|
||||||
|
|
||||||
@NonNull private MastodonApi mastodonApi;
|
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
NotificationPullJob(String domain, Context context) {
|
NotificationPullJob(Context context) {
|
||||||
this.mastodonApi = createMastodonApi(domain, context);
|
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
protected Result onRunJob(Params params) {
|
protected Result onRunJob(Params params) {
|
||||||
try {
|
|
||||||
Response<List<Notification>> notifications =
|
List<AccountEntity> accountList = new ArrayList<>(TuskyApplication.getAccountManager().getAllAccountsOrderedByActive());
|
||||||
mastodonApi.notifications(null, null, null).execute();
|
|
||||||
if (notifications.isSuccessful()) {
|
for(AccountEntity account: accountList) {
|
||||||
onNotificationsReceived(notifications.body());
|
|
||||||
} else {
|
if(account.getNotificationsEnabled()) {
|
||||||
return Result.FAILURE;
|
MastodonApi api = createMastodonApi(account.getDomain(), context);
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "getting Notifications for "+account.getFullName());
|
||||||
|
Response<List<Notification>> notifications =
|
||||||
|
api.notificationsWithAuth(String.format("Bearer %s", account.getAccessToken())).execute();
|
||||||
|
if (notifications.isSuccessful()) {
|
||||||
|
onNotificationsReceived(account, notifications.body());
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "error receiving notificationsEnabled");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, "error receiving notificationsEnabled", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return Result.FAILURE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.SUCCESS;
|
return Result.SUCCESS;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onNotificationsReceived(List<Notification> notificationList) {
|
private void onNotificationsReceived(AccountEntity account, List<Notification> notificationList) {
|
||||||
SharedPreferences notificationsPreferences = context.getSharedPreferences(
|
|
||||||
"Notifications", Context.MODE_PRIVATE);
|
BigInteger newId = new BigInteger(account.getLastNotificationId());
|
||||||
//make a copy of the string set, the returned instance should not be modified
|
|
||||||
Set<String> currentIds = new HashSet<>(notificationsPreferences.getStringSet(
|
BigInteger newestId = BigInteger.ZERO;
|
||||||
"current_ids", Collections.emptySet()));
|
|
||||||
for (Notification notification : notificationList) {
|
for(Notification notification: notificationList){
|
||||||
String id = notification.id;
|
|
||||||
if (!currentIds.contains(id)) {
|
BigInteger currentId = new BigInteger(notification.id);
|
||||||
currentIds.add(id);
|
|
||||||
NotificationManager.make(context, NOTIFY_ID, notification);
|
if(isBiggerThan(currentId, newestId)) {
|
||||||
|
newestId = currentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBiggerThan(currentId, newId)) {
|
||||||
|
account.setLastNotificationId(notification.id);
|
||||||
|
|
||||||
|
NotificationManager.make(context, notification, account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notificationsPreferences.edit()
|
|
||||||
.putStringSet("current_ids", currentIds)
|
account.setLastNotificationId(newestId.toString());
|
||||||
.apply();
|
TuskyApplication.getAccountManager().saveAccount(account);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBiggerThan(BigInteger newId, BigInteger lastShownNotificationId) {
|
||||||
|
|
||||||
|
return lastShownNotificationId.compareTo(newId) == - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,13 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky;
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
|
import com.keylesspalace.tusky.util.NotificationManager;
|
||||||
|
|
||||||
public class SplashActivity extends AppCompatActivity {
|
public class SplashActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
@ -28,16 +29,16 @@ public class SplashActivity extends AppCompatActivity {
|
||||||
|
|
||||||
/* Determine whether the user is currently logged in, and if so go ahead and load the
|
/* Determine whether the user is currently logged in, and if so go ahead and load the
|
||||||
* timeline. Otherwise, start the activity_login screen. */
|
* timeline. Otherwise, start the activity_login screen. */
|
||||||
SharedPreferences preferences = getSharedPreferences(
|
|
||||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
NotificationManager.deleteLegacyNotificationChannels(this);
|
||||||
String domain = preferences.getString("domain", null);
|
|
||||||
String accessToken = preferences.getString("accessToken", null);
|
AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
|
||||||
|
|
||||||
Intent intent;
|
Intent intent;
|
||||||
if (domain != null && accessToken != null) {
|
if (activeAccount != null) {
|
||||||
intent = new Intent(this, MainActivity.class);
|
intent = new Intent(this, MainActivity.class);
|
||||||
} else {
|
} else {
|
||||||
intent = new Intent(this, LoginActivity.class);
|
intent = LoginActivity.getIntent(this, false);
|
||||||
}
|
}
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
finish();
|
finish();
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.support.v7.app.AppCompatDelegate;
|
||||||
|
|
||||||
import com.evernote.android.job.JobManager;
|
import com.evernote.android.job.JobManager;
|
||||||
import com.jakewharton.picasso.OkHttp3Downloader;
|
import com.jakewharton.picasso.OkHttp3Downloader;
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.db.AppDatabase;
|
import com.keylesspalace.tusky.db.AppDatabase;
|
||||||
import com.keylesspalace.tusky.util.OkHttpUtils;
|
import com.keylesspalace.tusky.util.OkHttpUtils;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
|
@ -33,6 +34,7 @@ public class TuskyApplication extends Application {
|
||||||
public static final String APP_THEME_DEFAULT = "AppTheme:prefer:night";
|
public static final String APP_THEME_DEFAULT = "AppTheme:prefer:night";
|
||||||
|
|
||||||
private static AppDatabase db;
|
private static AppDatabase db;
|
||||||
|
private static AccountManager accountManager;
|
||||||
|
|
||||||
public static AppDatabase getDB() {
|
public static AppDatabase getDB() {
|
||||||
return db;
|
return db;
|
||||||
|
@ -71,5 +73,12 @@ public class TuskyApplication extends Application {
|
||||||
|
|
||||||
//necessary for Android < APi 21
|
//necessary for Android < APi 21
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||||
|
|
||||||
|
accountManager = new AccountManager();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public static AccountManager getAccountManager() {
|
||||||
|
return accountManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright 2017 Andrew Dawson
|
/* Copyright 2018 Conny Duck
|
||||||
*
|
*
|
||||||
* This file is a part of Tusky.
|
* This file is a part of Tusky.
|
||||||
*
|
*
|
||||||
|
@ -13,19 +13,19 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.receiver;
|
package com.keylesspalace.tusky.db
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
import android.arch.persistence.room.*
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
@Dao
|
||||||
import android.content.SharedPreferences;
|
interface AccountDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertOrReplace(account: AccountEntity): Long
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun delete(account: AccountEntity)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM AccountEntity ORDER BY id ASC")
|
||||||
|
fun loadAll(): List<AccountEntity>
|
||||||
|
|
||||||
public class NotificationClearBroadcastReceiver extends BroadcastReceiver {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
SharedPreferences notificationPreferences = context.getSharedPreferences("Notifications", Context.MODE_PRIVATE);
|
|
||||||
SharedPreferences.Editor editor = notificationPreferences.edit();
|
|
||||||
editor.putString("current", "[]");
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
/* Copyright 2018 Conny Duck
|
||||||
|
*
|
||||||
|
* 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.db
|
||||||
|
|
||||||
|
import android.arch.persistence.room.Entity
|
||||||
|
import android.arch.persistence.room.Index
|
||||||
|
import android.arch.persistence.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(indices = [Index(value = ["domain", "accountId"],
|
||||||
|
unique = true)])
|
||||||
|
data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
|
||||||
|
val domain: String,
|
||||||
|
var accessToken: String,
|
||||||
|
var isActive: Boolean,
|
||||||
|
var accountId: String = "",
|
||||||
|
var username: String = "",
|
||||||
|
var displayName: String = "",
|
||||||
|
var profilePictureUrl: String = "",
|
||||||
|
var notificationsEnabled: Boolean = true,
|
||||||
|
var notificationsMentioned: Boolean = true,
|
||||||
|
var notificationsFollowed: Boolean = true,
|
||||||
|
var notificationsReblogged: Boolean = true,
|
||||||
|
var notificationsFavorited: Boolean = true,
|
||||||
|
var notificationSound: Boolean = true,
|
||||||
|
var notificationVibration: Boolean = true,
|
||||||
|
var notificationLight: Boolean = true,
|
||||||
|
var lastNotificationId: String = "0",
|
||||||
|
var activeNotifications: String = "[]") {
|
||||||
|
|
||||||
|
val identifier: String
|
||||||
|
get() = "$domain:$accountId"
|
||||||
|
|
||||||
|
val fullName: String
|
||||||
|
get() = "@$username@$domain"
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as AccountEntity
|
||||||
|
|
||||||
|
if (id == other.id) return true
|
||||||
|
if (domain == other.domain && accountId == other.accountId) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + domain.hashCode()
|
||||||
|
result = 31 * result + accountId.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
190
app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
Normal file
190
app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
/* Copyright 2018 Conny Duck
|
||||||
|
*
|
||||||
|
* 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.db
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.keylesspalace.tusky.TuskyApplication
|
||||||
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class caches the account database and handles all account related operations
|
||||||
|
* @author ConnyDuck
|
||||||
|
*/
|
||||||
|
|
||||||
|
class AccountManager {
|
||||||
|
|
||||||
|
@Volatile var activeAccount: AccountEntity? = null
|
||||||
|
|
||||||
|
private var accounts: MutableList<AccountEntity> = mutableListOf()
|
||||||
|
private val accountDao: AccountDao = TuskyApplication.getDB().accountDao()
|
||||||
|
|
||||||
|
init {
|
||||||
|
accounts = accountDao.loadAll().toMutableList()
|
||||||
|
|
||||||
|
activeAccount = accounts.find { acc ->
|
||||||
|
acc.isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new empty account and makes it the active account.
|
||||||
|
* More account information has to be added later with [updateActiveAccount]
|
||||||
|
* or the account wont be saved to the database.
|
||||||
|
* @param accessToken the access token for the new account
|
||||||
|
* @param domain the domain of the accounts Mastodon instance
|
||||||
|
*/
|
||||||
|
fun addAccount(accessToken: String, domain: String) {
|
||||||
|
|
||||||
|
activeAccount?.let{
|
||||||
|
it.isActive = false
|
||||||
|
Log.d("AccountManager", "saving account with id "+it.id)
|
||||||
|
|
||||||
|
accountDao.insertOrReplace(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
activeAccount = AccountEntity(id = 0, domain = domain, accessToken = accessToken, isActive = true)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves an already known account to the database.
|
||||||
|
* New accounts must be created with [addAccount]
|
||||||
|
* @param account the account to save
|
||||||
|
*/
|
||||||
|
fun saveAccount(account: AccountEntity) {
|
||||||
|
if(account.id != 0L) {
|
||||||
|
Log.d("AccountManager", "saving account with id "+account.id)
|
||||||
|
val index = accounts.indexOf(account)
|
||||||
|
if (index != -1) {
|
||||||
|
accounts.removeAt(index)
|
||||||
|
accounts.add(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountDao.insertOrReplace(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the current account out by deleting all data of the account.
|
||||||
|
* @return the new active account, or null if no other account was found
|
||||||
|
*/
|
||||||
|
fun logActiveAccountOut() : AccountEntity? {
|
||||||
|
|
||||||
|
if(activeAccount == null) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
accounts.remove(activeAccount!!)
|
||||||
|
accountDao.delete(activeAccount!!)
|
||||||
|
|
||||||
|
if(accounts.size > 0) {
|
||||||
|
accounts[0].isActive = true
|
||||||
|
activeAccount = accounts[0]
|
||||||
|
accountDao.insertOrReplace(accounts[0])
|
||||||
|
} else {
|
||||||
|
activeAccount = null
|
||||||
|
}
|
||||||
|
return activeAccount
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* updates the current account with new information from the mastodon api
|
||||||
|
* and saves it in the database
|
||||||
|
* @param account the [Account] object returned from the api
|
||||||
|
*/
|
||||||
|
fun updateActiveAccount(account: Account) {
|
||||||
|
activeAccount?.let{
|
||||||
|
it.accountId = account.id
|
||||||
|
it.username = account.username
|
||||||
|
it.displayName = account.getDisplayName()
|
||||||
|
it.profilePictureUrl = account.avatar
|
||||||
|
|
||||||
|
Log.d("AccountManager", "id before save "+it.id)
|
||||||
|
it.id = accountDao.insertOrReplace(it)
|
||||||
|
Log.d("AccountManager", "id after save "+it.id)
|
||||||
|
|
||||||
|
|
||||||
|
val accountIndex = accounts.indexOf(it)
|
||||||
|
|
||||||
|
if(accountIndex != -1) {
|
||||||
|
//in case the user was already logged in with this account, remove the old information
|
||||||
|
accounts.removeAt(accountIndex)
|
||||||
|
accounts.add(accountIndex, it)
|
||||||
|
} else {
|
||||||
|
accounts.add(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* changes the active account
|
||||||
|
* @param accountId the database id of the new active account
|
||||||
|
*/
|
||||||
|
fun setActiveAccount(accountId: Long) {
|
||||||
|
|
||||||
|
activeAccount?.let{
|
||||||
|
it.isActive = false
|
||||||
|
accountDao.insertOrReplace(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
activeAccount = accounts.find { acc ->
|
||||||
|
acc.id == accountId
|
||||||
|
}
|
||||||
|
|
||||||
|
activeAccount?.let{
|
||||||
|
it.isActive = true
|
||||||
|
accountDao.insertOrReplace(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an immutable list of all accounts in the database with the active account first
|
||||||
|
*/
|
||||||
|
fun getAllAccountsOrderedByActive(): List<AccountEntity> {
|
||||||
|
accounts.sortWith (Comparator { l, r ->
|
||||||
|
when {
|
||||||
|
l.isActive && !r.isActive -> -1
|
||||||
|
r.isActive && !l.isActive -> 1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return accounts.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if at least one account has notifications enabled
|
||||||
|
*/
|
||||||
|
fun notificationsEnabled(): Boolean {
|
||||||
|
return accounts.any { it.notificationsEnabled }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an account by its database id
|
||||||
|
* @param accountId the id of the account
|
||||||
|
* @return the requested account or null if it was not found
|
||||||
|
*/
|
||||||
|
fun getAccountById(accountId: Long): AccountEntity? {
|
||||||
|
return accounts.find { acc ->
|
||||||
|
acc.id == accountId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -25,10 +25,11 @@ import android.support.annotation.NonNull;
|
||||||
* DB version & declare DAO
|
* DB version & declare DAO
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Database(entities = {TootEntity.class}, version = 4, exportSchema = false)
|
@Database(entities = {TootEntity.class, AccountEntity.class}, version = 4, exportSchema = false)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract TootDao tootDao();
|
public abstract TootDao tootDao();
|
||||||
|
public abstract AccountDao accountDao();
|
||||||
|
|
||||||
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
|
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -37,9 +37,12 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.MainActivity;
|
import com.keylesspalace.tusky.MainActivity;
|
||||||
|
import com.keylesspalace.tusky.TuskyApplication;
|
||||||
import com.keylesspalace.tusky.adapter.FooterViewHolder;
|
import com.keylesspalace.tusky.adapter.FooterViewHolder;
|
||||||
import com.keylesspalace.tusky.adapter.NotificationsAdapter;
|
import com.keylesspalace.tusky.adapter.NotificationsAdapter;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
import com.keylesspalace.tusky.entity.Attachment;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
@ -57,6 +60,7 @@ import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||||
import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -79,7 +83,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder for the notifications. Consider moving to the separate class to hide constructor
|
* Placeholder for the notificationsEnabled. Consider moving to the separate class to hide constructor
|
||||||
* and reuse in different places as needed.
|
* and reuse in different places as needed.
|
||||||
*/
|
*/
|
||||||
private static final class Placeholder {
|
private static final class Placeholder {
|
||||||
|
@ -200,7 +204,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
|
|
||||||
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
|
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
|
||||||
* guaranteed to be set until then.
|
* guaranteed to be set until then.
|
||||||
* Use a modified scroll listener that both loads more notifications as it goes, and hides
|
* Use a modified scroll listener that both loads more notificationsEnabled as it goes, and hides
|
||||||
* the compose button on down-scroll. */
|
* the compose button on down-scroll. */
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||||
|
@ -552,14 +556,13 @@ public class NotificationsFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
update(notifications, fromId, uptoId);
|
update(notifications, fromId, uptoId);
|
||||||
}
|
}
|
||||||
/* Set last update id for pull notifications so that we don't get notified
|
|
||||||
* about things we already loaded here */
|
|
||||||
getPrivatePreferences().edit()
|
|
||||||
.putString("lastUpdateId", fromId)
|
|
||||||
.apply();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveNewestNotificationId(notifications);
|
||||||
|
|
||||||
fulfillAnyQueuedFetches(fetchEnd);
|
fulfillAnyQueuedFetches(fetchEnd);
|
||||||
if (notifications.size() == 0 && adapter.getItemCount() == 1) {
|
if (notifications.size() == 0 && adapter.getItemCount() == 1) {
|
||||||
adapter.setFooterState(FooterViewHolder.State.EMPTY);
|
adapter.setFooterState(FooterViewHolder.State.EMPTY);
|
||||||
|
@ -581,6 +584,29 @@ public class NotificationsFragment extends SFragment implements
|
||||||
fulfillAnyQueuedFetches(fetchEnd);
|
fulfillAnyQueuedFetches(fetchEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void saveNewestNotificationId(List<Notification> notifications) {
|
||||||
|
AccountManager accountManager = TuskyApplication.getAccountManager();
|
||||||
|
AccountEntity account = accountManager.getActiveAccount();
|
||||||
|
BigInteger lastNoti = new BigInteger(account.getLastNotificationId());
|
||||||
|
|
||||||
|
for (Notification noti: notifications) {
|
||||||
|
BigInteger a = new BigInteger(noti.id);
|
||||||
|
if(isBiggerThan(a, lastNoti)) {
|
||||||
|
lastNoti = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "saving newest noti id: " + lastNoti);
|
||||||
|
|
||||||
|
account.setLastNotificationId(lastNoti.toString());
|
||||||
|
accountManager.saveAccount(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBiggerThan(BigInteger newId, BigInteger lastShownNotificationId) {
|
||||||
|
|
||||||
|
return lastShownNotificationId.compareTo(newId) == - 1;
|
||||||
|
}
|
||||||
|
|
||||||
private void update(@Nullable List<Notification> newNotifications, @Nullable String fromId,
|
private void update(@Nullable List<Notification> newNotifications, @Nullable String fromId,
|
||||||
@Nullable String uptoId) {
|
@Nullable String uptoId) {
|
||||||
if (ListUtils.isEmpty(newNotifications)) {
|
if (ListUtils.isEmpty(newNotifications)) {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.preference.CheckBoxPreference;
|
||||||
import android.preference.EditTextPreference;
|
import android.preference.EditTextPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
|
@ -27,7 +28,8 @@ import android.support.annotation.XmlRes;
|
||||||
import com.keylesspalace.tusky.BuildConfig;
|
import com.keylesspalace.tusky.BuildConfig;
|
||||||
import com.keylesspalace.tusky.PreferencesActivity;
|
import com.keylesspalace.tusky.PreferencesActivity;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.util.NotificationManager;
|
import com.keylesspalace.tusky.TuskyApplication;
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
|
|
||||||
public class PreferencesFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
|
public class PreferencesFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
SharedPreferences sharedPreferences;
|
SharedPreferences sharedPreferences;
|
||||||
|
@ -58,23 +60,28 @@ public class PreferencesFragment extends PreferenceFragment implements SharedPre
|
||||||
|
|
||||||
if(notificationPreferences != null) {
|
if(notificationPreferences != null) {
|
||||||
|
|
||||||
|
AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && activeAccount != null) {
|
||||||
|
notificationPreferences.setSummary(getString(R.string.pref_summary_notifications, activeAccount.getFullName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//on Android O and newer, launch the system notification settings instead of the app settings
|
//on Android O and newer, launch the system notification settings instead of the app settings
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|
||||||
NotificationManager.createNotificationChannels(getContext());
|
notificationPreferences.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS");
|
||||||
|
|
||||||
notificationPreferences.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
intent.putExtra("android.provider.extra.APP_PACKAGE", BuildConfig.APPLICATION_ID);
|
||||||
@Override
|
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS");
|
|
||||||
|
|
||||||
intent.putExtra("android.provider.extra.APP_PACKAGE", BuildConfig.APPLICATION_ID);
|
startActivity(intent);
|
||||||
|
return true;
|
||||||
startActivity(intent);
|
}
|
||||||
return true;
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
notificationPreferences.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
notificationPreferences.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||||
|
@ -122,6 +129,38 @@ public class PreferencesFragment extends PreferenceFragment implements SharedPre
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(preference == R.xml.notification_preferences) {
|
||||||
|
|
||||||
|
AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
|
||||||
|
|
||||||
|
if(activeAccount != null) {
|
||||||
|
|
||||||
|
CheckBoxPreference notificationPref = (CheckBoxPreference) findPreference("notificationsEnabled");
|
||||||
|
notificationPref.setChecked(activeAccount.getNotificationsEnabled());
|
||||||
|
|
||||||
|
CheckBoxPreference mentionedPref = (CheckBoxPreference) findPreference("notificationFilterMentions");
|
||||||
|
mentionedPref.setChecked(activeAccount.getNotificationsMentioned());
|
||||||
|
|
||||||
|
CheckBoxPreference followedPref = (CheckBoxPreference) findPreference("notificationFilterFollows");
|
||||||
|
followedPref.setChecked(activeAccount.getNotificationsFollowed());
|
||||||
|
|
||||||
|
CheckBoxPreference boostedPref = (CheckBoxPreference) findPreference("notificationFilterReblogs");
|
||||||
|
boostedPref.setChecked(activeAccount.getNotificationsReblogged());
|
||||||
|
|
||||||
|
CheckBoxPreference favoritedPref = (CheckBoxPreference) findPreference("notificationFilterFavourites");
|
||||||
|
favoritedPref.setChecked(activeAccount.getNotificationsFavorited());
|
||||||
|
|
||||||
|
CheckBoxPreference soundPref = (CheckBoxPreference) findPreference("notificationAlertSound");
|
||||||
|
soundPref.setChecked(activeAccount.getNotificationSound());
|
||||||
|
|
||||||
|
CheckBoxPreference vibrationPref = (CheckBoxPreference) findPreference("notificationAlertVibrate");
|
||||||
|
vibrationPref.setChecked(activeAccount.getNotificationVibration());
|
||||||
|
|
||||||
|
CheckBoxPreference lightPref = (CheckBoxPreference) findPreference("notificationAlertLight");
|
||||||
|
lightPref.setChecked(activeAccount.getNotificationLight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -150,15 +189,50 @@ public class PreferencesFragment extends PreferenceFragment implements SharedPre
|
||||||
@Override
|
@Override
|
||||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
||||||
String key) {
|
String key) {
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "httpProxyServer":
|
case "httpProxyServer":
|
||||||
case "httpProxyPort":
|
case "httpProxyPort":
|
||||||
updateSummary(key);
|
updateSummary(key);
|
||||||
case "httpProxyEnabled":
|
case "httpProxyEnabled":
|
||||||
httpProxyChanged = true;
|
httpProxyChanged = true;
|
||||||
break;
|
return;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
|
||||||
|
|
||||||
|
if(activeAccount != null) {
|
||||||
|
switch(key) {
|
||||||
|
case "notificationsEnabled":
|
||||||
|
activeAccount.setNotificationsEnabled(sharedPreferences.getBoolean(key, true));
|
||||||
|
break;
|
||||||
|
case "notificationFilterMentions":
|
||||||
|
activeAccount.setNotificationsMentioned(sharedPreferences.getBoolean(key, true));
|
||||||
|
break;
|
||||||
|
case "notificationFilterFollows":
|
||||||
|
activeAccount.setNotificationsFollowed(sharedPreferences.getBoolean(key, true));
|
||||||
|
break;
|
||||||
|
case "notificationFilterReblogs":
|
||||||
|
activeAccount.setNotificationsReblogged(sharedPreferences.getBoolean(key, true));
|
||||||
|
break;
|
||||||
|
case "notificationFilterFavourites":
|
||||||
|
activeAccount.setNotificationsFavorited(sharedPreferences.getBoolean(key, true));
|
||||||
|
break;
|
||||||
|
case "notificationAlertSound":
|
||||||
|
activeAccount.setNotificationSound(sharedPreferences.getBoolean(key, true));
|
||||||
|
break;
|
||||||
|
case "notificationAlertVibrate":
|
||||||
|
activeAccount.setNotificationVibration(sharedPreferences.getBoolean(key, true));
|
||||||
|
break;
|
||||||
|
case "notificationAlertLight":
|
||||||
|
activeAccount.setNotificationLight(sharedPreferences.getBoolean(key, true));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
TuskyApplication.getAccountManager().saveAccount(activeAccount);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSummary(String key) {
|
private void updateSummary(String key) {
|
||||||
|
|
|
@ -19,7 +19,6 @@ import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
@ -36,10 +35,12 @@ import com.keylesspalace.tusky.BaseActivity;
|
||||||
import com.keylesspalace.tusky.ComposeActivity;
|
import com.keylesspalace.tusky.ComposeActivity;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.ReportActivity;
|
import com.keylesspalace.tusky.ReportActivity;
|
||||||
|
import com.keylesspalace.tusky.TuskyApplication;
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||||
import com.keylesspalace.tusky.ViewTagActivity;
|
import com.keylesspalace.tusky.ViewTagActivity;
|
||||||
import com.keylesspalace.tusky.ViewThreadActivity;
|
import com.keylesspalace.tusky.ViewThreadActivity;
|
||||||
import com.keylesspalace.tusky.ViewVideoActivity;
|
import com.keylesspalace.tusky.ViewVideoActivity;
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
import com.keylesspalace.tusky.entity.Attachment;
|
||||||
import com.keylesspalace.tusky.entity.Relationship;
|
import com.keylesspalace.tusky.entity.Relationship;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
@ -73,9 +74,11 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
SharedPreferences preferences = getPrivatePreferences();
|
AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
|
||||||
loggedInAccountId = preferences.getString("loggedInAccountId", null);
|
if(activeAccount != null) {
|
||||||
loggedInUsername = preferences.getString("loggedInAccountUsername", null);
|
loggedInAccountId = activeAccount.getAccountId();
|
||||||
|
loggedInUsername = activeAccount.getUsername();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
package com.keylesspalace.tusky.network;
|
package com.keylesspalace.tusky.network;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.TuskyApplication;
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@ -17,37 +15,24 @@ import okhttp3.Response;
|
||||||
* Created by charlag on 31/10/17.
|
* Created by charlag on 31/10/17.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public final class AuthInterceptor implements Interceptor, SharedPreferences.OnSharedPreferenceChangeListener {
|
public final class AuthInterceptor implements Interceptor {
|
||||||
|
|
||||||
private static final String TOKEN_KEY = "accessToken";
|
public AuthInterceptor() { }
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private String token;
|
|
||||||
|
|
||||||
public AuthInterceptor(Context context) {
|
|
||||||
SharedPreferences preferences = context.getSharedPreferences(
|
|
||||||
context.getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
|
||||||
token = preferences.getString(TOKEN_KEY, null);
|
|
||||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response intercept(@NonNull Chain chain) throws IOException {
|
public Response intercept(@NonNull Chain chain) throws IOException {
|
||||||
|
|
||||||
|
AccountEntity currentAccount = TuskyApplication.getAccountManager().getActiveAccount();
|
||||||
|
|
||||||
Request originalRequest = chain.request();
|
Request originalRequest = chain.request();
|
||||||
|
|
||||||
Request.Builder builder = originalRequest.newBuilder();
|
Request.Builder builder = originalRequest.newBuilder();
|
||||||
if (token != null) {
|
if (currentAccount != null) {
|
||||||
builder.header("Authorization", String.format("Bearer %s", token));
|
builder.header("Authorization", String.format("Bearer %s", currentAccount.getAccessToken()));
|
||||||
}
|
}
|
||||||
Request newRequest = builder.build();
|
Request newRequest = builder.build();
|
||||||
|
|
||||||
return chain.proceed(newRequest);
|
return chain.proceed(newRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
|
||||||
if (key.equals(TOKEN_KEY)) {
|
|
||||||
token = sharedPreferences.getString(TOKEN_KEY, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import retrofit2.http.DELETE;
|
||||||
import retrofit2.http.Field;
|
import retrofit2.http.Field;
|
||||||
import retrofit2.http.FormUrlEncoded;
|
import retrofit2.http.FormUrlEncoded;
|
||||||
import retrofit2.http.GET;
|
import retrofit2.http.GET;
|
||||||
|
import retrofit2.http.Header;
|
||||||
import retrofit2.http.Multipart;
|
import retrofit2.http.Multipart;
|
||||||
import retrofit2.http.PATCH;
|
import retrofit2.http.PATCH;
|
||||||
import retrofit2.http.POST;
|
import retrofit2.http.POST;
|
||||||
|
@ -81,6 +82,9 @@ public interface MastodonApi {
|
||||||
@Query("max_id") String maxId,
|
@Query("max_id") String maxId,
|
||||||
@Query("since_id") String sinceId,
|
@Query("since_id") String sinceId,
|
||||||
@Query("limit") Integer limit);
|
@Query("limit") Integer limit);
|
||||||
|
@GET("api/v1/notifications")
|
||||||
|
Call<List<Notification>> notificationsWithAuth(
|
||||||
|
@Header("Authorization") String auth);
|
||||||
@POST("api/v1/notifications/clear")
|
@POST("api/v1/notifications/clear")
|
||||||
Call<ResponseBody> clearNotifications();
|
Call<ResponseBody> clearNotifications();
|
||||||
@GET("api/v1/notifications/{id}")
|
@GET("api/v1/notifications/{id}")
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/* Copyright 2018 Conny Duck
|
||||||
|
*
|
||||||
|
* 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.receiver
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.TuskyApplication
|
||||||
|
import com.keylesspalace.tusky.util.NotificationManager
|
||||||
|
|
||||||
|
class NotificationClearBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
|
||||||
|
val accountId = intent.getLongExtra(NotificationManager.ACCOUNT_ID, -1)
|
||||||
|
|
||||||
|
val accountManager = TuskyApplication.getAccountManager()
|
||||||
|
val account = accountManager.getAccountById(accountId)
|
||||||
|
if (account != null) {
|
||||||
|
account.activeNotifications = "[]"
|
||||||
|
accountManager.saveAccount(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,14 +16,13 @@
|
||||||
package com.keylesspalace.tusky.util;
|
package com.keylesspalace.tusky.util;
|
||||||
|
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationChannelGroup;
|
||||||
import android.app.PendingIntent;
|
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.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.provider.Settings;
|
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;
|
||||||
|
@ -32,8 +31,10 @@ import android.support.v4.content.ContextCompat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.MainActivity;
|
import com.keylesspalace.tusky.MainActivity;
|
||||||
import com.keylesspalace.tusky.NotificationPullJobCreator;
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.TuskyApplication;
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
|
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
|
||||||
import com.keylesspalace.tusky.view.RoundedTransformation;
|
import com.keylesspalace.tusky.view.RoundedTransformation;
|
||||||
|
@ -47,11 +48,13 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class NotificationManager {
|
public class NotificationManager {
|
||||||
|
|
||||||
|
/** constants used in Intents */
|
||||||
|
public static final String ACCOUNT_ID = "account_id";
|
||||||
|
|
||||||
private static final String TAG = "NotificationManager";
|
private static final String TAG = "NotificationManager";
|
||||||
|
|
||||||
/**
|
/** notification channels used on Android O+ **/
|
||||||
* notification channels used on Android O+
|
|
||||||
**/
|
|
||||||
private static final String CHANNEL_MENTION = "CHANNEL_MENTION";
|
private static final String CHANNEL_MENTION = "CHANNEL_MENTION";
|
||||||
private static final String CHANNEL_FOLLOW = "CHANNEL_FOLLOW";
|
private static final String CHANNEL_FOLLOW = "CHANNEL_FOLLOW";
|
||||||
private static final String CHANNEL_BOOST = "CHANNEL_BOOST";
|
private static final String CHANNEL_BOOST = "CHANNEL_BOOST";
|
||||||
|
@ -62,22 +65,17 @@ public class NotificationManager {
|
||||||
* the state of the existing notification to reflect the new interaction.
|
* the state of the existing notification to reflect the new interaction.
|
||||||
*
|
*
|
||||||
* @param context to access application preferences and services
|
* @param context to access application preferences and services
|
||||||
* @param notifyId an arbitrary number to reference this notification for any future action
|
|
||||||
* @param body a new Mastodon notification
|
* @param body a new Mastodon notification
|
||||||
|
* @param account the account for which the notification should be shown
|
||||||
*/
|
*/
|
||||||
public static void make(final Context context, final int notifyId, Notification body) {
|
|
||||||
final SharedPreferences preferences =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
final SharedPreferences notificationPreferences = context.getSharedPreferences(
|
|
||||||
"Notifications", Context.MODE_PRIVATE);
|
|
||||||
|
|
||||||
if (!filterNotification(preferences, body)) {
|
public static void make(final Context context, Notification body, AccountEntity account) {
|
||||||
|
|
||||||
|
if (!filterNotification(account, body)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createNotificationChannels(context);
|
String rawCurrentNotifications = account.getActiveNotifications();
|
||||||
|
|
||||||
String rawCurrentNotifications = notificationPreferences.getString("current", "[]");
|
|
||||||
JSONArray currentNotifications;
|
JSONArray currentNotifications;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -102,34 +100,40 @@ public class NotificationManager {
|
||||||
currentNotifications.put(body.account.getDisplayName());
|
currentNotifications.put(body.account.getDisplayName());
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationPreferences.edit()
|
account.setActiveNotifications(currentNotifications.toString());
|
||||||
.putString("current", currentNotifications.toString())
|
|
||||||
.apply();
|
//no need to save account, this will be done in the calling function
|
||||||
|
|
||||||
Intent resultIntent = new Intent(context, MainActivity.class);
|
Intent resultIntent = new Intent(context, MainActivity.class);
|
||||||
resultIntent.putExtra("tab_position", 1);
|
resultIntent.putExtra(ACCOUNT_ID, account.getId());
|
||||||
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
|
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
|
||||||
stackBuilder.addParentStack(MainActivity.class);
|
stackBuilder.addParentStack(MainActivity.class);
|
||||||
stackBuilder.addNextIntent(resultIntent);
|
stackBuilder.addNextIntent(resultIntent);
|
||||||
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,
|
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent((int)account.getId(),
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
|
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
|
||||||
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, 0, deleteIntent,
|
deleteIntent.putExtra(ACCOUNT_ID, account.getId());
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, (int)account.getId(), deleteIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(body))
|
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(account, body))
|
||||||
.setSmallIcon(R.drawable.ic_notify)
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
.setContentIntent(resultPendingIntent)
|
.setContentIntent(resultPendingIntent)
|
||||||
.setDeleteIntent(deletePendingIntent)
|
.setDeleteIntent(deletePendingIntent)
|
||||||
.setColor(ContextCompat.getColor(context, (R.color.primary)))
|
.setColor(ContextCompat.getColor(context, (R.color.primary)))
|
||||||
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
|
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
|
||||||
|
|
||||||
setupPreferences(preferences, builder);
|
setupPreferences(account, builder);
|
||||||
|
|
||||||
if (currentNotifications.length() == 1) {
|
if (currentNotifications.length() == 1) {
|
||||||
builder.setContentTitle(titleForType(context, body))
|
builder.setContentTitle(titleForType(context, body))
|
||||||
.setContentText(truncateWithEllipses(bodyForType(body), 40));
|
.setContentText(bodyForType(body));
|
||||||
|
|
||||||
|
if(body.type == Notification.Type.MENTION) {
|
||||||
|
builder.setStyle(new NotificationCompat.BigTextStyle()
|
||||||
|
.bigText(bodyForType(body)));
|
||||||
|
}
|
||||||
|
|
||||||
//load the avatar synchronously
|
//load the avatar synchronously
|
||||||
Bitmap accountAvatar;
|
Bitmap accountAvatar;
|
||||||
|
@ -149,7 +153,7 @@ public class NotificationManager {
|
||||||
try {
|
try {
|
||||||
String format = context.getString(R.string.notification_title_summary);
|
String format = context.getString(R.string.notification_title_summary);
|
||||||
String title = String.format(format, currentNotifications.length());
|
String title = String.format(format, currentNotifications.length());
|
||||||
String text = truncateWithEllipses(joinNames(context, currentNotifications), 40);
|
String text = joinNames(context, currentNotifications);
|
||||||
builder.setContentTitle(title)
|
builder.setContentTitle(title)
|
||||||
.setContentText(text);
|
.setContentText(text);
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
|
@ -157,24 +161,29 @@ public class NotificationManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
builder.setSubText(account.getFullName());
|
||||||
builder.setVisibility(android.app.Notification.VISIBILITY_PRIVATE);
|
|
||||||
builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
|
builder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE);
|
||||||
}
|
builder.setCategory(NotificationCompat.CATEGORY_SOCIAL);
|
||||||
|
|
||||||
android.app.NotificationManager notificationManager = (android.app.NotificationManager)
|
android.app.NotificationManager notificationManager = (android.app.NotificationManager)
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE);
|
context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
notificationManager.notify(notifyId, builder.build());
|
notificationManager.notify((int)account.getId(), builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void createNotificationChannels(Context context) {
|
public static void createNotificationChannelsForAccount(AccountEntity account, Context context) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|
||||||
android.app.NotificationManager mNotificationManager =
|
android.app.NotificationManager mNotificationManager =
|
||||||
(android.app.NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
(android.app.NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
String[] channelIds = new String[]{CHANNEL_MENTION, CHANNEL_FOLLOW, CHANNEL_BOOST, CHANNEL_FAVOURITE};
|
String[] channelIds = new String[]{
|
||||||
|
CHANNEL_MENTION+account.getIdentifier(),
|
||||||
|
CHANNEL_FOLLOW+account.getIdentifier(),
|
||||||
|
CHANNEL_BOOST+account.getIdentifier(),
|
||||||
|
CHANNEL_FAVOURITE+account.getIdentifier()};
|
||||||
int[] channelNames = {
|
int[] channelNames = {
|
||||||
R.string.notification_channel_mention_name,
|
R.string.notification_channel_mention_name,
|
||||||
R.string.notification_channel_follow_name,
|
R.string.notification_channel_follow_name,
|
||||||
|
@ -190,6 +199,11 @@ public class NotificationManager {
|
||||||
|
|
||||||
List<NotificationChannel> channels = new ArrayList<>(4);
|
List<NotificationChannel> channels = new ArrayList<>(4);
|
||||||
|
|
||||||
|
NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName());
|
||||||
|
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
mNotificationManager.createNotificationChannelGroup(channelGroup);
|
||||||
|
|
||||||
for (int i = 0; i < channelIds.length; i++) {
|
for (int i = 0; i < channelIds.length; i++) {
|
||||||
String id = channelIds[i];
|
String id = channelIds[i];
|
||||||
String name = context.getString(channelNames[i]);
|
String name = context.getString(channelNames[i]);
|
||||||
|
@ -201,6 +215,7 @@ public class NotificationManager {
|
||||||
channel.enableLights(true);
|
channel.enableLights(true);
|
||||||
channel.enableVibration(true);
|
channel.enableVibration(true);
|
||||||
channel.setShowBadge(true);
|
channel.setShowBadge(true);
|
||||||
|
channel.setGroup(account.getIdentifier());
|
||||||
channels.add(channel);
|
channels.add(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,20 +225,48 @@ public class NotificationManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void clearNotifications(@Nullable Context context) {
|
public static void deleteNotificationChannelsForAccount(AccountEntity account, Context context) {
|
||||||
if(context != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
SharedPreferences notificationPreferences =
|
|
||||||
context.getSharedPreferences("Notifications", Context.MODE_PRIVATE);
|
android.app.NotificationManager mNotificationManager =
|
||||||
notificationPreferences.edit().putString("current", "[]").apply();
|
(android.app.NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
mNotificationManager.deleteNotificationChannelGroup(account.getIdentifier());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteLegacyNotificationChannels(Context context) {
|
||||||
|
// delete the notification channels that where used before the multi account mode was introduced to avoid confusion
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|
||||||
|
android.app.NotificationManager mNotificationManager =
|
||||||
|
(android.app.NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
mNotificationManager.deleteNotificationChannel(CHANNEL_MENTION);
|
||||||
|
mNotificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE);
|
||||||
|
mNotificationManager.deleteNotificationChannel(CHANNEL_BOOST);
|
||||||
|
mNotificationManager.deleteNotificationChannel(CHANNEL_FOLLOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearNotificationsForActiveAccount(Context context) {
|
||||||
|
AccountManager accountManager = TuskyApplication.getAccountManager();
|
||||||
|
AccountEntity account = accountManager.getActiveAccount();
|
||||||
|
if (account != null) {
|
||||||
|
account.setActiveNotifications("[]");
|
||||||
|
accountManager.saveAccount(account);
|
||||||
|
|
||||||
android.app.NotificationManager manager = (android.app.NotificationManager)
|
android.app.NotificationManager manager = (android.app.NotificationManager)
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE);
|
context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
manager.cancel(NotificationPullJobCreator.NOTIFY_ID);
|
manager.cancel((int)account.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean filterNotification(SharedPreferences preferences,
|
private static boolean filterNotification(AccountEntity account,
|
||||||
Notification notification) {
|
Notification notification) {
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
@ -233,56 +276,47 @@ public class NotificationManager {
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
default:
|
default:
|
||||||
case MENTION:
|
case MENTION:
|
||||||
return preferences.getBoolean("notificationFilterMentions", true);
|
return account.getNotificationsMentioned();
|
||||||
case FOLLOW:
|
case FOLLOW:
|
||||||
return preferences.getBoolean("notificationFilterFollows", true);
|
return account.getNotificationsFollowed();
|
||||||
case REBLOG:
|
case REBLOG:
|
||||||
return preferences.getBoolean("notificationFilterReblogs", true);
|
return account.getNotificationsReblogged();
|
||||||
case FAVOURITE:
|
case FAVOURITE:
|
||||||
return preferences.getBoolean("notificationFilterFavourites", true);
|
return account.getNotificationsFavorited();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getChannelId(Notification notification) {
|
private static String getChannelId(AccountEntity account, Notification notification) {
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
default:
|
default:
|
||||||
case MENTION:
|
case MENTION:
|
||||||
return CHANNEL_MENTION;
|
return CHANNEL_MENTION+account.getIdentifier();
|
||||||
case FOLLOW:
|
case FOLLOW:
|
||||||
return CHANNEL_FOLLOW;
|
return CHANNEL_FOLLOW+account.getIdentifier();
|
||||||
case REBLOG:
|
case REBLOG:
|
||||||
return CHANNEL_BOOST;
|
return CHANNEL_BOOST+account.getIdentifier();
|
||||||
case FAVOURITE:
|
case FAVOURITE:
|
||||||
return CHANNEL_FAVOURITE;
|
return CHANNEL_FAVOURITE+account.getIdentifier();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("SameParameterValue")
|
private static void setupPreferences(AccountEntity account,
|
||||||
private static String truncateWithEllipses(String string, int limit) {
|
|
||||||
if (string.length() < limit) {
|
|
||||||
return string;
|
|
||||||
} else {
|
|
||||||
return string.substring(0, limit - 3) + "...";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setupPreferences(SharedPreferences preferences,
|
|
||||||
NotificationCompat.Builder builder) {
|
NotificationCompat.Builder builder) {
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
return; //do nothing on Android O or newer, the system uses the channel settings anyway
|
return; //do nothing on Android O or newer, the system uses the channel settings anyway
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.getBoolean("notificationAlertSound", true)) {
|
if (account.getNotificationSound()) {
|
||||||
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
|
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.getBoolean("notificationAlertVibrate", false)) {
|
if (account.getNotificationVibration()) {
|
||||||
builder.setVibrate(new long[]{500, 500});
|
builder.setVibrate(new long[]{500, 500});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.getBoolean("notificationAlertLight", false)) {
|
if (account.getNotificationLight()) {
|
||||||
builder.setLights(0xFF00FF8F, 300, 1000);
|
builder.setLights(0xFF00FF8F, 300, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -326,7 +360,7 @@ public class NotificationManager {
|
||||||
private static String bodyForType(Notification notification) {
|
private static String bodyForType(Notification notification) {
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
case FOLLOW:
|
case FOLLOW:
|
||||||
return notification.account.username;
|
return "@"+notification.account.username;
|
||||||
case MENTION:
|
case MENTION:
|
||||||
case FAVOURITE:
|
case FAVOURITE:
|
||||||
case REBLOG:
|
case REBLOG:
|
||||||
|
|
13
app/src/main/res/anim/explode.xml
Normal file
13
app/src/main/res/anim/explode.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||||
|
<scale
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="300"
|
||||||
|
android:fromXScale="0"
|
||||||
|
android:fromYScale="0"
|
||||||
|
android:pivotX="50%"
|
||||||
|
android:pivotY="50%"
|
||||||
|
android:toXScale="1"
|
||||||
|
android:toYScale="1" >
|
||||||
|
</scale>
|
||||||
|
</set>
|
BIN
app/src/main/res/drawable-xxhdpi/ic_reblog_dark_.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_reblog_dark_.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
|
@ -16,7 +16,16 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:background="@android:color/transparent" />
|
android:background="@android:color/transparent">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composeAvatar"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:layout_gravity="right|end"
|
||||||
|
android:layout_width="?attr/actionBarSize"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
<!--content description will be set in code -->
|
||||||
|
</android.support.v7.widget.Toolbar>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:textSize="?attr/status_text_small"
|
android:textSize="?attr/status_text_small"
|
||||||
|
|
|
@ -1,82 +1,99 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:fillViewport="true"
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
tools:context="com.keylesspalace.tusky.LoginActivity">
|
tools:context="com.keylesspalace.tusky.LoginActivity">
|
||||||
|
|
||||||
<LinearLayout
|
<ScrollView
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:padding="16dp"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center"
|
android:fillViewport="true"
|
||||||
android:layout_height="wrap_content">
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
<ImageView
|
|
||||||
android:layout_width="147dp"
|
|
||||||
android:layout_height="160dp"
|
|
||||||
android:layout_marginBottom="50dp"
|
|
||||||
android:src="@drawable/elephant_friend"
|
|
||||||
android:contentDescription="@null" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/login_input"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:gravity="center"
|
||||||
<android.support.design.widget.TextInputLayout
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_width="250dp">
|
|
||||||
<android.support.design.widget.TextInputEditText
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:inputType="textUri"
|
|
||||||
android:hint="@string/hint_domain"
|
|
||||||
android:ems="10"
|
|
||||||
android:id="@+id/edit_text_domain" />
|
|
||||||
</android.support.design.widget.TextInputLayout>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/button_login"
|
|
||||||
android:layout_width="250dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:text="@string/action_login" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="250dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:id="@+id/text_error" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="250dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingTop="5dp"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:id="@+id/whats_an_instance"
|
|
||||||
android:text="@string/link_whats_an_instance" />
|
|
||||||
</LinearLayout>
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/login_loading"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="wrap_content"
|
android:padding="16dp">
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<ProgressBar
|
<ImageView
|
||||||
android:layout_gravity="center"
|
android:layout_width="147dp"
|
||||||
|
android:layout_height="160dp"
|
||||||
|
android:layout_marginBottom="50dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/elephant_friend" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/loginInputLayout"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
<TextView
|
|
||||||
android:paddingTop="10dp"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:layout_width="250dp"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/login_connection"/>
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<android.support.design.widget.TextInputLayout
|
||||||
|
android:layout_width="250dp"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<android.support.design.widget.TextInputEditText
|
||||||
|
android:id="@+id/domainEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="@string/hint_domain"
|
||||||
|
android:inputType="textUri" />
|
||||||
|
</android.support.design.widget.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/loginButton"
|
||||||
|
android:layout_width="250dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:text="@string/action_login"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/whatsAnInstanceTextView"
|
||||||
|
android:layout_width="250dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="6dp"
|
||||||
|
android:text="@string/link_whats_an_instance"
|
||||||
|
android:textAlignment="center" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/loginLoadingLayout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="250dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:text="@string/login_connection"
|
||||||
|
android:textAlignment="center" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<android.support.v7.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
</LinearLayout>
|
</android.support.constraint.ConstraintLayout>
|
||||||
</ScrollView>
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
* This is the for follow notifications, the layout for the follows/following listings on account
|
* This is the for folnotificationsEnabledions, the layout for the follows/following listings on account
|
||||||
* pages are instead in item_account.xml.
|
* pages are instead in item_account.xml.
|
||||||
-->
|
-->
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><!--This applies only to favourite and reblog notifications.-->
|
<?xml version="1.0" encoding="utf-8"?><!--This applies only to favourite and rebnotificationsEnabledions.-->
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
|
|
@ -55,7 +55,6 @@
|
||||||
<string name="action_compose">حرر</string>
|
<string name="action_compose">حرر</string>
|
||||||
<string name="action_login">التسجيل بواسطة ماستدون</string>
|
<string name="action_login">التسجيل بواسطة ماستدون</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>
|
||||||
|
|
|
@ -60,7 +60,6 @@
|
||||||
<string name="action_compose">Redacta</string>
|
<string name="action_compose">Redacta</string>
|
||||||
<string name="action_login">Inicia sessió amb Mastodon</string>
|
<string name="action_login">Inicia sessió amb Mastodon</string>
|
||||||
<string name="action_logout">Tanca sessió</string>
|
<string name="action_logout">Tanca sessió</string>
|
||||||
<string name="action_logout_confirm">Vols tancar la sessió?</string>
|
|
||||||
<string name="action_follow">Segueix</string>
|
<string name="action_follow">Segueix</string>
|
||||||
<string name="action_unfollow">Deixa de seguir</string>
|
<string name="action_unfollow">Deixa de seguir</string>
|
||||||
<string name="action_block">Bloca</string>
|
<string name="action_block">Bloca</string>
|
||||||
|
|
|
@ -216,7 +216,6 @@
|
||||||
<string name="pref_title_show_boosts">Zeige Boosts</string>
|
<string name="pref_title_show_boosts">Zeige Boosts</string>
|
||||||
<string name="pref_title_pull_notification_check_interval">Überprüfungsintervall</string>
|
<string name="pref_title_pull_notification_check_interval">Überprüfungsintervall</string>
|
||||||
<string name="no_content">leer</string>
|
<string name="no_content">leer</string>
|
||||||
<string name="action_logout_confirm">Willst du dich wirklich ausloggen?</string>
|
|
||||||
<string name="action_hide_media">Verstecke Medien</string>
|
<string name="action_hide_media">Verstecke Medien</string>
|
||||||
<string name="pref_title_status_filter">Timeline-Filter</string>
|
<string name="pref_title_status_filter">Timeline-Filter</string>
|
||||||
<string name="title_saved_toot">Gespeicherte Tröts</string>
|
<string name="title_saved_toot">Gespeicherte Tröts</string>
|
||||||
|
|
|
@ -93,7 +93,6 @@
|
||||||
<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_accept">Accepter</string>
|
||||||
<string name="action_logout_confirm">Voulez-vous vous déconnectez ?</string>
|
|
||||||
<string name="action_reject">Rejeter</string>
|
<string name="action_reject">Rejeter</string>
|
||||||
<string name="action_undo">Annuler</string>
|
<string name="action_undo">Annuler</string>
|
||||||
<string name="action_view_follow_requests">Demandes de suivi</string>
|
<string name="action_view_follow_requests">Demandes de suivi</string>
|
||||||
|
|
|
@ -57,7 +57,6 @@
|
||||||
<string name="action_compose">Szerkeszt</string>
|
<string name="action_compose">Szerkeszt</string>
|
||||||
<string name="action_login">Bejelentkezés Mastodon-al</string>
|
<string name="action_login">Bejelentkezés Mastodon-al</string>
|
||||||
<string name="action_logout">Kijelentkezés</string>
|
<string name="action_logout">Kijelentkezés</string>
|
||||||
<string name="action_logout_confirm">Ki szeretne jelentkezni?</string>
|
|
||||||
<string name="action_follow">Követ</string>
|
<string name="action_follow">Követ</string>
|
||||||
<string name="action_unfollow">Követőktől eltávolít</string>
|
<string name="action_unfollow">Követőktől eltávolít</string>
|
||||||
<string name="action_block">Blokkol</string>
|
<string name="action_block">Blokkol</string>
|
||||||
|
|
|
@ -57,7 +57,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>
|
||||||
|
|
|
@ -51,7 +51,6 @@
|
||||||
<string name="action_compose">Schrijven</string>
|
<string name="action_compose">Schrijven</string>
|
||||||
<string name="action_login">Inloggen bij Mastodon</string>
|
<string name="action_login">Inloggen bij Mastodon</string>
|
||||||
<string name="action_logout">Uitloggen</string>
|
<string name="action_logout">Uitloggen</string>
|
||||||
<string name="action_logout_confirm">Wil je echt uitloggen?</string>
|
|
||||||
<string name="action_follow">Volgen</string>
|
<string name="action_follow">Volgen</string>
|
||||||
<string name="action_unfollow">Ontvolgen</string>
|
<string name="action_unfollow">Ontvolgen</string>
|
||||||
<string name="action_block">Blokkeren</string>
|
<string name="action_block">Blokkeren</string>
|
||||||
|
|
|
@ -68,9 +68,10 @@
|
||||||
<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">Śledź</string>
|
<string name="action_follow">Śledź</string>
|
||||||
<string name="action_unfollow">Przestań śledzić</string>
|
<string name="action_unfollow">Przestań śledzić</string>
|
||||||
|
|
||||||
<string name="action_block">Zablokuj</string>
|
<string name="action_block">Zablokuj</string>
|
||||||
<string name="action_unblock">Odblokuj</string>
|
<string name="action_unblock">Odblokuj</string>
|
||||||
<string name="action_report">Zgłoś</string>
|
<string name="action_report">Zgłoś</string>
|
||||||
|
|
|
@ -60,7 +60,6 @@
|
||||||
<string name="action_compose">Compor</string>
|
<string name="action_compose">Compor</string>
|
||||||
<string name="action_login">Entrar com Mastodon</string>
|
<string name="action_login">Entrar com Mastodon</string>
|
||||||
<string name="action_logout">Sair</string>
|
<string name="action_logout">Sair</string>
|
||||||
<string name="action_logout_confirm">Deseja sair?</string>
|
|
||||||
<string name="action_follow">Seguir</string>
|
<string name="action_follow">Seguir</string>
|
||||||
<string name="action_unfollow">Deixar de seguir</string>
|
<string name="action_unfollow">Deixar de seguir</string>
|
||||||
<string name="action_block">Bloquear</string>
|
<string name="action_block">Bloquear</string>
|
||||||
|
|
|
@ -61,7 +61,6 @@
|
||||||
<string name="action_compose">Написать</string>
|
<string name="action_compose">Написать</string>
|
||||||
<string name="action_login">Войти</string>
|
<string name="action_login">Войти</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>
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
<color name="view_video_background">#000000</color>
|
<color name="view_video_background">#000000</color>
|
||||||
<color name="toolbar_view_media">#8f000000</color>
|
<color name="toolbar_view_media">#8f000000</color>
|
||||||
<color name="semi_transparent">#33000000</color>
|
<color name="semi_transparent">#33000000</color>
|
||||||
|
<color name="header_background_filter">#44000000</color>
|
||||||
|
|
||||||
<!--Dark Theme Colors-->
|
<!--Dark Theme Colors-->
|
||||||
<color name="color_primary_dark">#4c5368</color>
|
<color name="color_primary_dark">#4c5368</color>
|
||||||
<color name="color_primary_dark_dark">#313543</color> <!--Dark Dark-->
|
<color name="color_primary_dark_dark">#313543</color> <!--Dark Dark-->
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
<string name="action_compose">Compose</string>
|
<string name="action_compose">Compose</string>
|
||||||
<string name="action_login">Login with Mastodon</string>
|
<string name="action_login">Login with Mastodon</string>
|
||||||
<string name="action_logout">Log Out</string>
|
<string name="action_logout">Log Out</string>
|
||||||
<string name="action_logout_confirm">Do you wish to logout</string>
|
<string name="action_logout_confirm">Are you sure you want to log out of the account %1$s?</string>
|
||||||
<string name="action_follow">Follow</string>
|
<string name="action_follow">Follow</string>
|
||||||
<string name="action_unfollow">Unfollow</string>
|
<string name="action_unfollow">Unfollow</string>
|
||||||
<string name="action_block">Block</string>
|
<string name="action_block">Block</string>
|
||||||
|
@ -157,6 +157,7 @@
|
||||||
<string name="pref_title_notification_settings">Notifications</string>
|
<string name="pref_title_notification_settings">Notifications</string>
|
||||||
<string name="pref_title_edit_notification_settings">Edit Notifications</string>
|
<string name="pref_title_edit_notification_settings">Edit Notifications</string>
|
||||||
<string name="pref_title_notifications_enabled">Notifications</string>
|
<string name="pref_title_notifications_enabled">Notifications</string>
|
||||||
|
<string name="pref_summary_notifications">for account %1$s</string>
|
||||||
<string name="pref_title_pull_notification_check_interval">Check Interval</string>
|
<string name="pref_title_pull_notification_check_interval">Check Interval</string>
|
||||||
<string name="pref_title_notification_alerts">Alerts</string>
|
<string name="pref_title_notification_alerts">Alerts</string>
|
||||||
<string name="pref_title_notification_alert_sound">Notify with a sound</string>
|
<string name="pref_title_notification_alert_sound">Notify with a sound</string>
|
||||||
|
@ -230,7 +231,7 @@
|
||||||
<string name="notification_channel_boost_name">Boosts</string>
|
<string name="notification_channel_boost_name">Boosts</string>
|
||||||
<string name="notification_channel_boost_description">Notifications when your toots get boosted</string>
|
<string name="notification_channel_boost_description">Notifications when your toots get boosted</string>
|
||||||
<string name="notification_channel_favourite_name">Favourites</string>
|
<string name="notification_channel_favourite_name">Favourites</string>
|
||||||
<string name="notification_channel_favourite_description">Notifications when your toots get mark as favourite</string>
|
<string name="notification_channel_favourite_description">Notifications when your toots get marked as favourite</string>
|
||||||
|
|
||||||
|
|
||||||
<string name="notification_mention_format">%s mentioned you</string>
|
<string name="notification_mention_format">%s mentioned you</string>
|
||||||
|
@ -285,9 +286,16 @@
|
||||||
<string name="title_media">Media</string>
|
<string name="title_media">Media</string>
|
||||||
<string name="replying_to">Replying to @%s</string>
|
<string name="replying_to">Replying to @%s</string>
|
||||||
<string name="load_more_placeholder_text">load more</string>
|
<string name="load_more_placeholder_text">load more</string>
|
||||||
|
|
||||||
|
<string name="add_account_name">Add Account</string>
|
||||||
|
<string name="add_account_description">Add new Mastodon Account</string>
|
||||||
|
|
||||||
<string name="action_lists">Lists</string>
|
<string name="action_lists">Lists</string>
|
||||||
<string name="title_lists">Lists</string>
|
<string name="title_lists">Lists</string>
|
||||||
<string name="title_list_timeline">List timeline</string>
|
<string name="title_list_timeline">List timeline</string>
|
||||||
|
|
||||||
|
<string name="compose_active_account_description">Posting with account %1$s</string>
|
||||||
|
|
||||||
<string name="error_failed_set_caption">Failed to set caption</string>
|
<string name="error_failed_set_caption">Failed to set caption</string>
|
||||||
<string name="hint_describe_for_visually_impaired">Describe for visually impaired</string>
|
<string name="hint_describe_for_visually_impaired">Describe for visually impaired</string>
|
||||||
<string name="action_set_caption">Set caption</string>
|
<string name="action_set_caption">Set caption</string>
|
||||||
|
|
Loading…
Reference in a new issue