diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java index e873d0c11..55d91046a 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java @@ -37,6 +37,8 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst String TWIDERE_PROJECT_EMAIL = "twidere.project@gmail.com"; String TWIDERE_PACKAGE_NAME = "org.mariotaku.twidere"; + String ACCOUNT_TYPE = "org.mariotaku.twidere.account"; + String LOGTAG = TWIDERE_APP_NAME; String USER_NICKNAME_PREFERENCES_NAME = "user_nicknames"; diff --git a/twidere/build.gradle b/twidere/build.gradle index 354a62adc..1d9d69aa8 100644 --- a/twidere/build.gradle +++ b/twidere/build.gradle @@ -175,6 +175,7 @@ dependencies { compile 'nl.komponents.kovenant:kovenant:3.3.0' compile 'nl.komponents.kovenant:kovenant-android:3.3.0' compile 'nl.komponents.kovenant:kovenant-functional:3.3.0' + compile 'nl.komponents.kovenant:kovenant-combine:3.3.0' } task svgToDrawable(type: SvgDrawableTask) { diff --git a/twidere/src/androidTest/java/org/mariotaku/twidere/activity/ImagePageFragmentTest.kt b/twidere/src/androidTest/java/org/mariotaku/twidere/activity/ImagePageFragmentTest.kt deleted file mode 100644 index aadb3a656..000000000 --- a/twidere/src/androidTest/java/org/mariotaku/twidere/activity/ImagePageFragmentTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.mariotaku.twidere.activity - -import android.net.Uri -import org.junit.Assert.assertEquals -import org.junit.Test -import org.mariotaku.twidere.fragment.ImagePageFragment - -/** - * Created by mariotaku on 16/3/3. - */ -class ImagePageFragmentTest { - - @Test - @Throws(Exception::class) - fun testReplaceTwitterMediaUri() { - assertEquals("https://pbs.twimg.com/media/DEADBEEF.png:large", - ImagePageFragment.replaceTwitterMediaUri(Uri.parse( - "https://pbs.twimg.com/media/DEADBEEF.png:large")).toString()) - assertEquals("https://pbs.twimg.com/media/DEADBEEF.png:orig", - ImagePageFragment.replaceTwitterMediaUri(Uri.parse( - "https://pbs.twimg.com/media/DEADBEEF.png:orig")).toString()) - assertEquals("https://pbs.twimg.com/media/DEADBEEF.png:large", - ImagePageFragment.replaceTwitterMediaUri(Uri.parse( - "https://pbs.twimg.com/media/DEADBEEF.jpg:large")).toString()) - assertEquals("https://pbs.twimg.com/media/DEADBEEF.png:large", - ImagePageFragment.replaceTwitterMediaUri(Uri.parse( - "https://pbs.twimg.com/media/DEADBEEF.jpg:orig")).toString()) - assertEquals("https://pbs.twimg.com/media/DEADBEEF.png", - ImagePageFragment.replaceTwitterMediaUri(Uri.parse( - "https://pbs.twimg.com/media/DEADBEEF.jpg")).toString()) - assertEquals("https://pbs.twimg.com/media/DEADBEEF.png:", - ImagePageFragment.replaceTwitterMediaUri(Uri.parse( - "https://pbs.twimg.com/media/DEADBEEF.jpg:")).toString()) - assertEquals("https://example.com/media/DEADBEEF.jpg", - ImagePageFragment.replaceTwitterMediaUri(Uri.parse( - "https://example.com/media/DEADBEEF.jpg")).toString()) - } -} \ No newline at end of file diff --git a/twidere/src/androidTest/java/org/mariotaku/twidere/test/account/MigrationTest.kt b/twidere/src/androidTest/java/org/mariotaku/twidere/test/account/MigrationTest.kt new file mode 100644 index 000000000..9279e3a83 --- /dev/null +++ b/twidere/src/androidTest/java/org/mariotaku/twidere/test/account/MigrationTest.kt @@ -0,0 +1,41 @@ +package org.mariotaku.twidere.test.account + +import android.accounts.Account +import android.accounts.AccountManager +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.mariotaku.ktextension.Bundle +import org.mariotaku.twidere.TwidereConstants.ACCOUNT_TYPE +import org.mariotaku.twidere.extension.model.account_name +import org.mariotaku.twidere.model.util.ParcelableAccountUtils +import org.mariotaku.twidere.provider.TwidereDataStore +import org.mariotaku.twidere.provider.TwidereDataStore.Accounts +import org.mariotaku.twidere.util.support.AccountManagerSupport + +/** + * Created by mariotaku on 2016/12/2. + */ +@RunWith(AndroidJUnit4::class) +class MigrationTest { + @Test + fun testMigration() { + + val context = InstrumentationRegistry.getTargetContext() + + val am = AccountManager.get(context) + + am.getAccountsByType(ACCOUNT_TYPE).map { account -> + AccountManagerSupport.removeAccount(am, account, null, null, null) + } + + ParcelableAccountUtils.getAccounts(context).forEach { pAccount -> + val account = Account(pAccount.account_name, ACCOUNT_TYPE) + val userdata = Bundle { + this[Accounts.ACCOUNT_KEY] + } + am.addAccountExplicitly(account, null, userdata) + } + } +} \ No newline at end of file diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/support/AccountManagerSupport.java b/twidere/src/main/java/org/mariotaku/twidere/util/support/AccountManagerSupport.java new file mode 100644 index 000000000..ffc19e590 --- /dev/null +++ b/twidere/src/main/java/org/mariotaku/twidere/util/support/AccountManagerSupport.java @@ -0,0 +1,87 @@ +package org.mariotaku.twidere.util.support; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.app.Activity; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.RequiresApi; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * Created by mariotaku on 2016/12/2. + */ + +public class AccountManagerSupport { + public static AccountManagerFuture removeAccount(AccountManager am, Account account, + Activity activity, + final AccountManagerCallback callback, + Handler handler) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + return AccountManagerSupportL.removeAccount(am, account, activity, callback, handler); + } + //noinspection deprecation + final AccountManagerFuture future = am.removeAccount(account, new AccountManagerCallback() { + @Override + public void run(AccountManagerFuture future) { + callback.run(new BooleanToBundleAccountManagerFuture(future)); + } + }, handler); + return new BooleanToBundleAccountManagerFuture(future); + } + + private static class AccountManagerSupportL { + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1) + static AccountManagerFuture removeAccount(AccountManager am, Account account, + Activity activity, + AccountManagerCallback callback, + Handler handler) { + return am.removeAccount(account, activity, callback, handler); + } + } + + private static class BooleanToBundleAccountManagerFuture implements AccountManagerFuture { + + private final AccountManagerFuture future; + + BooleanToBundleAccountManagerFuture(AccountManagerFuture future) { + this.future = future; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return future.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return future.isCancelled(); + } + + @Override + public boolean isDone() { + return future.isDone(); + } + + @Override + public Bundle getResult() throws OperationCanceledException, IOException, AuthenticatorException { + Bundle result = new Bundle(); + result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, future.getResult()); + return result; + } + + @Override + public Bundle getResult(long timeout, TimeUnit unit) throws OperationCanceledException, IOException, AuthenticatorException { + Bundle result = new Bundle(); + result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, future.getResult(timeout, unit)); + return result; + } + } +} diff --git a/twidere/src/main/kotlin/org/mariotaku/ktextension/PromiseArrayCombine.kt b/twidere/src/main/kotlin/org/mariotaku/ktextension/PromiseArrayCombine.kt new file mode 100644 index 000000000..2adbe1218 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/ktextension/PromiseArrayCombine.kt @@ -0,0 +1,52 @@ +package org.mariotaku.ktextension + +import nl.komponents.kovenant.Deferred +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.deferred +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReferenceArray + +/** + * Created by mariotaku on 2016/12/2. + */ +fun combine(promises: List>): Promise, E> { + return concreteCombine(promises) +} + +fun concreteCombine(promises: List>): Promise, E> { + val deferred = deferred, E>() + + val results = AtomicReferenceArray(promises.size) + val successCount = AtomicInteger(promises.size) + + fun createArray(): List { + return (0 until results.length()).map { results[it] } + } + + fun Promise.registerSuccess(idx: Int) { + success { v -> + results.set(idx, v) + if (successCount.decrementAndGet() == 0) { + deferred.resolve(createArray()) + } + } + } + + fun Deferred.registerFail(promises: List>) { + val failCount = AtomicInteger(0) + promises.forEach { promise -> + promise.fail { e -> + if (failCount.incrementAndGet() == 1) { + this.reject(e) + } + } + } + } + + promises.forEachIndexed { idx, promise -> + promise.registerSuccess(idx) + } + deferred.registerFail(promises) + + return deferred.promise +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt index 2d71f6dd1..b93a73c96 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt @@ -19,6 +19,8 @@ package org.mariotaku.twidere.activity +import android.accounts.AccountAuthenticatorResponse +import android.accounts.AccountManager import android.app.Activity import android.app.Dialog import android.content.ContentValues @@ -79,6 +81,7 @@ import org.mariotaku.twidere.util.OAuthPasswordAuthenticator.* import org.mariotaku.twidere.util.view.ConsumerKeySecretValidator import java.lang.ref.WeakReference + class SignInActivity : BaseActivity(), OnClickListener, TextWatcher { private var apiUrlFormat: String? = null private var authType: Int = 0 @@ -89,15 +92,66 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher { private var noVersionSuffix: Boolean = false private var signInTask: AbstractSignInTask? = null - override fun afterTextChanged(s: Editable) { + private var accountAuthenticatorResponse: AccountAuthenticatorResponse? = null + private var accountAuthenticatorResult: Bundle? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + accountAuthenticatorResponse = intent.getParcelableExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE) + accountAuthenticatorResponse?.onRequestContinued() + + setContentView(R.layout.activity_sign_in) + + if (savedInstanceState != null) { + apiUrlFormat = savedInstanceState.getString(Accounts.API_URL_FORMAT) + authType = savedInstanceState.getInt(Accounts.AUTH_TYPE) + sameOAuthSigningUrl = savedInstanceState.getBoolean(Accounts.SAME_OAUTH_SIGNING_URL) + consumerKey = savedInstanceState.getString(Accounts.CONSUMER_KEY)?.trim() + consumerSecret = savedInstanceState.getString(Accounts.CONSUMER_SECRET)?.trim() + apiChangeTimestamp = savedInstanceState.getLong(EXTRA_API_LAST_CHANGE) + } + + val isTwipOMode = authType == AuthType.TWIP_O_MODE + usernamePasswordContainer.visibility = if (isTwipOMode) View.GONE else View.VISIBLE + signInSignUpContainer.orientation = if (isTwipOMode) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL + + editUsername.addTextChangedListener(this) + editPassword.addTextChangedListener(this) + + signIn.setOnClickListener(this) + signUp.setOnClickListener(this) + passwordSignIn.setOnClickListener(this) + + val color = ColorStateList.valueOf(ContextCompat.getColor(this, + R.color.material_light_green)) + ViewCompat.setBackgroundTintList(signIn, color) + + + val consumerKey = preferences.getString(KEY_CONSUMER_KEY, null) + val consumerSecret = preferences.getString(KEY_CONSUMER_SECRET, null) + if (BuildConfig.SHOW_CUSTOM_TOKEN_DIALOG && savedInstanceState == null && + !preferences.getBoolean(KEY_CONSUMER_KEY_SECRET_SET, false) && + !Utils.isCustomConsumerKeySecret(consumerKey, consumerSecret)) { + val df = SetConsumerKeySecretDialogFragment() + df.isCancelable = false + df.show(supportFragmentManager, "set_consumer_key_secret") + } + + updateSignInType() + setSignInButton() } - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { - + override fun onDestroy() { + loaderManager.destroyLoader(0) + super.onDestroy() } - public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_sign_in, menu) + return true + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { REQUEST_EDIT_API -> { if (resultCode == Activity.RESULT_OK) { @@ -121,6 +175,27 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher { super.onActivityResult(requestCode, resultCode, data) } + override fun finish() { + accountAuthenticatorResponse?.let { response -> + // send the result bundle back if set, otherwise send an error. + if (accountAuthenticatorResult != null) { + response.onResult(accountAuthenticatorResult) + } else { + response.onError(AccountManager.ERROR_CODE_CANCELED, "canceled") + } + accountAuthenticatorResponse = null + } + super.finish() + } + + override fun afterTextChanged(s: Editable) { + + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + + } + internal fun updateSignInType() { when (authType) { AuthType.XAUTH, AuthType.BASIC -> { @@ -162,16 +237,6 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher { } } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_sign_in, menu) - return true - } - - public override fun onDestroy() { - loaderManager.destroyLoader(0) - super.onDestroy() - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> { @@ -237,54 +302,11 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher { super.onSaveInstanceState(outState) } + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { setSignInButton() } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_sign_in) - - if (savedInstanceState != null) { - apiUrlFormat = savedInstanceState.getString(Accounts.API_URL_FORMAT) - authType = savedInstanceState.getInt(Accounts.AUTH_TYPE) - sameOAuthSigningUrl = savedInstanceState.getBoolean(Accounts.SAME_OAUTH_SIGNING_URL) - consumerKey = savedInstanceState.getString(Accounts.CONSUMER_KEY)?.trim() - consumerSecret = savedInstanceState.getString(Accounts.CONSUMER_SECRET)?.trim() - apiChangeTimestamp = savedInstanceState.getLong(EXTRA_API_LAST_CHANGE) - } - - val isTwipOMode = authType == AuthType.TWIP_O_MODE - usernamePasswordContainer.visibility = if (isTwipOMode) View.GONE else View.VISIBLE - signInSignUpContainer.orientation = if (isTwipOMode) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL - - editUsername.addTextChangedListener(this) - editPassword.addTextChangedListener(this) - - signIn.setOnClickListener(this) - signUp.setOnClickListener(this) - passwordSignIn.setOnClickListener(this) - - val color = ColorStateList.valueOf(ContextCompat.getColor(this, - R.color.material_light_green)) - ViewCompat.setBackgroundTintList(signIn, color) - - - val consumerKey = preferences.getString(KEY_CONSUMER_KEY, null) - val consumerSecret = preferences.getString(KEY_CONSUMER_SECRET, null) - if (BuildConfig.SHOW_CUSTOM_TOKEN_DIALOG && savedInstanceState == null && - !preferences.getBoolean(KEY_CONSUMER_KEY_SECRET_SET, false) && - !Utils.isCustomConsumerKeySecret(consumerKey, consumerSecret)) { - val df = SetConsumerKeySecretDialogFragment() - df.isCancelable = false - df.show(supportFragmentManager, "set_consumer_key_secret") - } - - updateSignInType() - setSignInButton() - } - internal fun doLogin() { if (signInTask != null && signInTask!!.status == AsyncTask.Status.RUNNING) { signInTask!!.cancel(true) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/ParcelableAccountExtension.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/ParcelableAccountExtension.kt new file mode 100644 index 000000000..508a4a5c7 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/ParcelableAccountExtension.kt @@ -0,0 +1,12 @@ +package org.mariotaku.twidere.extension.model + +import org.mariotaku.twidere.model.ParcelableAccount +import org.mariotaku.twidere.model.UserKey + +/** + * Created by mariotaku on 2016/12/2. + */ + +val ParcelableAccount.account_name: String + get() = UserKey(screen_name, account_key.host).toString() + diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/service/AccountAuthenticatorService.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/service/AccountAuthenticatorService.kt index 3c1b25e31..162ce739d 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/service/AccountAuthenticatorService.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/service/AccountAuthenticatorService.kt @@ -10,7 +10,9 @@ import android.content.Intent import android.os.Bundle import android.os.IBinder import org.mariotaku.ktextension.set +import org.mariotaku.twidere.TwidereConstants import org.mariotaku.twidere.activity.SignInActivity +import org.mariotaku.twidere.util.support.AccountManagerSupport /**