Commit ceb3d438 authored by Thomas Markiewicz's avatar Thomas Markiewicz

Merge branch 'tm-hub-integration' into 'master'

Integrated with Librem Hub

See merge request !7
parents 57d0975e d953da6f
Pipeline #52097 passed with stages
in 10 minutes and 20 seconds
......@@ -4,6 +4,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- integration with system-wide Librem One accounts
### Changed
- onboarding screen to log in with Librem One account
## [1.2.1]
### Changed
......
......@@ -3,8 +3,17 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.keylesspalace.tusky">
<uses-permission
android:name="android.permission.GET_ACCOUNTS"
android:maxSdkVersion="22" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.MANAGE_ACCOUNTS"
android:maxSdkVersion="22" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.USE_CREDENTIALS"
android:maxSdkVersion="22" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" /> <!-- For notifications -->
<uses-permission
......
......@@ -15,18 +15,22 @@
package com.keylesspalace.tusky
import android.accounts.AccountManager
import android.accounts.AccountManagerFuture
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Color
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.webkit.WebView
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.browser.customtabs.CustomTabsIntent
import com.google.android.material.snackbar.Snackbar
......@@ -45,6 +49,7 @@ import retrofit2.Callback
import retrofit2.Response
import javax.inject.Inject
class LoginActivity : BaseActivity(), Injectable {
@Inject
......@@ -90,8 +95,6 @@ class LoginActivity : BaseActivity(), Injectable {
} else {
toolbar.visibility = View.GONE
}
}
override fun requiresLogin(): Boolean {
......@@ -391,6 +394,10 @@ class LoginActivity : BaseActivity(), Injectable {
accountManager.addAccount(accessToken, domain)
startMainActivity()
}
private fun startMainActivity() {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
......
......@@ -15,6 +15,9 @@
package com.keylesspalace.tusky;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
......@@ -95,9 +98,6 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
private static final long DRAWER_ITEM_SAVED_TOOT = 4;
private static final long DRAWER_ITEM_ACCOUNT_SETTINGS = 5;
private static final long DRAWER_ITEM_SETTINGS = 6;
private static final long DRAWER_ITEM_SUPPORT = 7;
private static final long DRAWER_ITEM_POLICY = 8;
private static final long DRAWER_ITEM_STAY_SAFE = 9;
private static final long DRAWER_ITEM_ABOUT = 10;
private static final long DRAWER_ITEM_LOG_OUT = 11;
private static final long DRAWER_ITEM_FOLLOW_REQUESTS = 12;
......@@ -260,9 +260,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
@Override
protected void onResume() {
super.onResume();
NotificationHelper.clearNotificationsForActiveAccount(this, accountManager);
syncLibremOneAccounts();
}
@Override
......@@ -406,11 +405,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
listItems.add(new DividerDrawerItem());
listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_ACCOUNT_SETTINGS).withName(R.string.action_view_account_preferences).withSelectable(false).withIcon(R.drawable.ic_account_settings).withIconTintingEnabled(true));
listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_SETTINGS).withName(R.string.action_view_preferences).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_settings));
listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_SUPPORT).withName(R.string.support).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_help));
listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_POLICY).withName(R.string.policy).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_assignment));
listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_STAY_SAFE).withName(R.string.stay_safe).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_do_not_disturb_on));
listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_ABOUT).withName(R.string.about_title_activity).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_info));
// tom: add links here
listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_LOG_OUT).withName(R.string.action_logout).withSelectable(false).withIcon(R.drawable.ic_logout).withIconTintingEnabled(true));
drawer = new DrawerBuilder()
......@@ -437,12 +432,6 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
} else if (drawerItemIdentifier == DRAWER_ITEM_SETTINGS) {
Intent intent = PreferencesActivity.newIntent(MainActivity.this, PreferencesActivity.GENERAL_PREFERENCES);
startActivityWithSlideInAnimation(intent);
} else if (drawerItemIdentifier == DRAWER_ITEM_SUPPORT) {
displayInWebView(R.string.support_url);
} else if (drawerItemIdentifier == DRAWER_ITEM_POLICY) {
displayInWebView(R.string.policy_url);
} else if (drawerItemIdentifier == DRAWER_ITEM_STAY_SAFE) {
displayInWebView(R.string.stay_safe_url);
} else if (drawerItemIdentifier == DRAWER_ITEM_ABOUT) {
Intent intent = new Intent(MainActivity.this, AboutActivity.class);
startActivityWithSlideInAnimation(intent);
......@@ -460,7 +449,6 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
} else if (drawerItemIdentifier == DRAWER_ITEM_LISTS) {
startActivityWithSlideInAnimation(ListsActivity.newIntent(this));
}
}
return false;
......@@ -565,6 +553,13 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
.setMessage(getString(R.string.action_logout_confirm, activeAccount.getFullName()))
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
android.accounts.AccountManager
.get(this)
.invalidateAuthToken(
"librem.one",
activeAccount.getAccessToken()
);
NotificationHelper.deleteNotificationChannelsForAccount(accountManager.getActiveAccount(), MainActivity.this);
cacheUpdater.clearForUser(activeAccount.getId());
conversationRepository.deleteCacheForAccount(activeAccount.getId());
......@@ -577,7 +572,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
Intent intent;
if (newAccount == null) {
intent = LoginActivity.getIntent(MainActivity.this, false);
intent = OnboardingActivity.getIntent(MainActivity.this);
} else {
intent = new Intent(MainActivity.this, MainActivity.class);
}
......@@ -660,10 +655,50 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
headerResult.setActiveProfile(accountManager.getActiveAccount().getId());
}
private void syncLibremOneAccounts() {
AccountManager am = AccountManager.get(MainActivity.this);
android.accounts.Account[] libremOneAccounts = am.getAccountsByType("librem.one");
for (android.accounts.Account libremOneAccount : libremOneAccounts) {
AccountEntity acc = accountManager.getAccountByUsername(
libremOneAccount.name.split("@")[0],
"social.librem.one");
if( acc == null) {
// need to add libremOneAccount to social
// but first get its token
am.getAuthToken(
libremOneAccount,
"social",
null,
true,
new AccountManagerCallback<Bundle>() {
@Override
public void run(AccountManagerFuture<Bundle> future) {
try {
Bundle result = future.getResult();
String username = result.getString(AccountManager.KEY_ACCOUNT_NAME).split("@")[0];
String token = result.getString(AccountManager.KEY_AUTHTOKEN);
long id = accountManager.addAccount(token, "social.librem.one", username);
changeAccount(id, null);
} catch (Exception e) {
e.printStackTrace();
}
}
},
null);
}
}
}
private static void onFetchUserInfoFailure(Throwable throwable) {
Log.e(TAG, "Failed to fetch user info. " + throwable.getMessage());
}
@Nullable
@Override
public FloatingActionButton getActionButton() {
......
package com.keylesspalace.tusky
import android.accounts.AccountManager
import android.accounts.AccountManagerCallback
import android.accounts.AccountManagerFuture
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.webkit.WebView
import android.os.Handler
import android.widget.Button
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.keylesspalace.tusky.di.Injectable
class OnboardingActivity : AppCompatActivity() {
class OnboardingActivity : BaseActivity(), Injectable {
companion object {
@JvmStatic
fun getIntent(context: Context): Intent {
val intent = Intent(context, OnboardingActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
return intent
}
}
var numLibremOneAccounts = 0
var processedLibremOneAccounts = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_onboarding)
findViewById<Button>(R.id.buttonRegister).setOnClickListener {
displayInWebView(R.string.register_url)
}
findViewById<Button>(R.id.buttonLoginLibremOne).setOnClickListener {
val am = AccountManager.get(this)
val accounts = am.getAccountsByType("librem.one")
val options = Bundle()
numLibremOneAccounts = accounts.size
if( accounts.isNotEmpty()) {
for (a in accounts) {
am.getAuthToken(
a,
"social",
options,
true,
OnTokenAcquired(),
Handler {
false
}
)
}
} else {
findViewById<Button>(R.id.buttonSupport).setOnClickListener {
displayInWebView(R.string.support_url)
if( !isLibremOneInstalled() ) {
askToInstallLibremOne()
} else {
am.addAccount(
"librem.one",
"social",
null,
null,
this,
OnTokenAcquired(),
null
)
}
}
}
findViewById<Button>(R.id.buttonPolicy).setOnClickListener {
displayInWebView(R.string.policy_url)
findViewById<Button>(R.id.buttonLoginOther).setOnClickListener {
startLoginActivity()
}
}
override fun requiresLogin(): Boolean {
return false
}
private fun isLibremOneInstalled(): Boolean {
var found = true
findViewById<Button>(R.id.buttonPolicy).setOnClickListener {
displayInWebView(R.string.policy_url)
try {
packageManager.getPackageInfo("one.librem.one", 0)
} catch (e: PackageManager.NameNotFoundException) {
found = false
}
findViewById<Button>(R.id.buttonStaySafe).setOnClickListener {
displayInWebView(R.string.stay_safe_url)
return found
}
private fun askToInstallLibremOne() {
val builder = AlertDialog.Builder(this)
builder.setMessage(R.string.install_librem_one_message)
.setTitle(R.string.install_librem_one)
.setPositiveButton(R.string.install,
DialogInterface.OnClickListener { dialog, id ->
startInstallLibremOneActivity()
})
.setNegativeButton(R.string.cancel,
DialogInterface.OnClickListener { dialog, id ->
// User cancelled the dialog
})
builder.create().show()
}
private fun startInstallLibremOneActivity() {
val goToMarket = Intent(Intent.ACTION_VIEW)
.setData(Uri.parse("https://play.google.com/store/apps/details?id=one.librem.one"))
goToMarket.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
startActivity(goToMarket)
}
private fun startLoginActivity() {
val intent = Intent(this, LoginActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
finish()
}
private fun startMainActivity() {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
overridePendingTransition(R.anim.explode, R.anim.explode)
}
private inner class OnTokenAcquired : AccountManagerCallback<Bundle> {
override fun run(result: AccountManagerFuture<Bundle>) {
// Get the result of the operation from the AccountManagerFuture.
val bundle = result.result
// Launch Librem One account login activity if requested
val launch = bundle.get(AccountManager.KEY_INTENT) as? Intent
if (launch != null) {
startActivityForResult(launch, REQUEST_CODE)
return
}
// Otherwise the token is a named value in the bundle. The name of the value
// is stored in the constant AccountManager.KEY_AUTHTOKEN.
val token: String? = bundle.getString(AccountManager.KEY_AUTHTOKEN)
if(token == null) {
// new account?
val am = AccountManager.get(this@OnboardingActivity)
val accounts = am.getAccountsByType("librem.one")
for (a in accounts) {
am.getAuthToken(
a,
"social",
null,
true,
OnTokenAcquired(),
Handler {
false
}
)
numLibremOneAccounts++;
}
} else {
val username: String = bundle.getString(AccountManager.KEY_ACCOUNT_NAME).split('@')[0]
// accountManager in this case comes from app's BaseActivity()
accountManager.addAccount(token, "social.librem.one", username)
}
// if this is the last account token we've added,
// move on to main activity
processedLibremOneAccounts++
if(processedLibremOneAccounts == numLibremOneAccounts) {
startMainActivity()
}
}
}
findViewById<Button>(R.id.buttonLogin).setOnClickListener {
val intent = Intent(this, LoginActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
finish()
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// Check which request we're responding to
if (requestCode == REQUEST_CODE) {
// Make sure the request was successful
if (resultCode == Activity.RESULT_OK) {
val token = data?.extras?.getString(AccountManager.KEY_AUTHTOKEN)
if(token!=null) {
accountManager.addAccount(token, "social.librem.one")
startMainActivity()
}
}
}
}
private fun displayInWebView(id: Int) {
val url = this.getString(id)
val wv = WebView(this)
wv.loadUrl(url)
AlertDialog.Builder(this)
.setView(wv)
.setPositiveButton(android.R.string.ok, null)
.show()
companion object {
@JvmStatic
var REQUEST_CODE = 123;
@JvmStatic
fun getIntent(context: Context): Intent {
val intent = Intent(context, OnboardingActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
return intent
}
}
}
......@@ -35,6 +35,7 @@ class AccountManager(db: AppDatabase) {
private val accountDao: AccountDao = db.accountDao()
init {
accounts = accountDao.loadAll().toMutableList()
activeAccount = accounts.find { acc ->
......@@ -49,7 +50,7 @@ class AccountManager(db: AppDatabase) {
* @param accessToken the access token for the new account
* @param domain the domain of the accounts Mastodon instance
*/
fun addAccount(accessToken: String, domain: String) {
fun addAccount(accessToken: String, domain: String, username: String = ""): Long {
activeAccount?.let {
it.isActive = false
......@@ -60,8 +61,13 @@ class AccountManager(db: AppDatabase) {
val maxAccountId = accounts.maxBy { it.id }?.id ?: 0
val newAccountId = maxAccountId + 1
activeAccount = AccountEntity(id = newAccountId, domain = domain.toLowerCase(), accessToken = accessToken, isActive = true)
activeAccount = AccountEntity(id = newAccountId, username = username, domain = domain.toLowerCase(), accessToken = accessToken, isActive = true)
activeAccount?.let {
accountDao.insertOrReplace(it)
accounts.add(it)
}
return newAccountId
}
/**
......@@ -74,7 +80,6 @@ class AccountManager(db: AppDatabase) {
Log.d(TAG, "saveAccount: saving account with id " + account.id)
accountDao.insertOrReplace(account)
}
}
/**
......@@ -98,7 +103,6 @@ class AccountManager(db: AppDatabase) {
activeAccount = null
}
return activeAccount
}
}
......@@ -190,4 +194,16 @@ class AccountManager(db: AppDatabase) {
}
}
/**
* Finds an account by its username and domain
* @param username the username of the account
* @param domain the domain of the account
* @return the requested account or null if it was not found
*/
fun getAccountByUsername(username: String, domain: String): AccountEntity? {
return accounts.find { acc ->
acc.username == username && acc.domain == domain
}
}
}
\ No newline at end of file
......@@ -71,6 +71,9 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesLoginActivity(): LoginActivity
@ContributesAndroidInjector
abstract fun contributesOnboardingActivity(): OnboardingActivity
@ContributesAndroidInjector
abstract fun contributesSplashActivity(): SplashActivity
......
......@@ -34,44 +34,33 @@
android:layout_marginStart="16dp"
android:layout_marginRight="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="32dp"
android:text="@string/onboarding_message" />
<Button
android:id="@+id/buttonRegister"
android:id="@+id/buttonInstallHub"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_marginTop="16dp"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_marginRight="16dp"
android:layout_marginEnd="16dp"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:text="@string/register" />
<Button
android:id="@+id/buttonSupport"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_marginRight="16dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:text="@string/support" />
android:text="@string/install_librem_one" />
<Button
android:id="@+id/buttonPolicy"
android:id="@+id/buttonLoginLibremOne"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_marginRight="16dp"
android:layout_marginEnd="16dp"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:text="@string/policy" />
android:layout_width="match_parent"
android:text="@string/action_login_librem_one" />
<Button
android:id="@+id/buttonStaySafe"
android:id="@+id/buttonLoginOther"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
......@@ -79,17 +68,9 @@
android:layout_marginEnd="16dp"
android:layout_width="match_parent"
style="@style/Widget.MaterialComponents.Button.TextButton"
</