diff --git a/LICENSE b/LICENSE index 5f2dd7fc..2e895888 100644 --- a/LICENSE +++ b/LICENSE @@ -79,7 +79,7 @@ Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General +does Less to protect the userAccount's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many @@ -102,7 +102,7 @@ operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is +users' freedom, it does ensure that the userAccount of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. @@ -280,7 +280,7 @@ Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one +directing the userAccount to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding @@ -289,22 +289,22 @@ of these things: Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified + userAccount can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the + that the userAccount who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, + copy of the library already present on the userAccount's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is + the userAccount installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials + least three years, to give the same userAccount the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. @@ -312,8 +312,8 @@ of these things: from a designated place, offer equivalent access to copy the above specified materials from the same place. - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. + e) Verify that the userAccount has already received a copy of these + materials or that you have already sent this userAccount a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for diff --git a/app/build.gradle b/app/build.gradle index 67dd5799..321fb929 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { defaultConfig { minSdkVersion rootProject.minSdkVersion targetSdkVersion rootProject.targetSdkVersion - versionCode 37 - versionName "0.6" + versionCode 38 + versionName "0.7" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -47,52 +47,37 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "androidx.core:core-ktx:$kotlin_android_extensions_version" + // Room Library + implementation "androidx.room:room-runtime:2.1.0" + implementation "androidx.room:room-ktx:2.1.0" + kapt "androidx.room:room-compiler:2.1.0" + + // For ViewModel lifecycle + kapt "androidx.lifecycle:lifecycle-compiler:2.2.0-alpha02" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0-alpha02" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha02" + //rxjava Dependencies implementation 'io.reactivex:rxandroid:1.1.0' implementation 'io.reactivex:rxjava:1.3.0' - // For Local Unit Tests - // Required -- JUnit 4 framework testImplementation 'junit:junit:4.12' - // Part of Instrumented Tests or Local Test - - // Optional -- Mockito framework - testImplementation 'org.mockito:mockito-core:2.8.9' - - // Optional -- Robolectric library - - //testImplementation "org.robolectric:robolectric:3.0" - // For android test support (it depends on emulator) androidTestImplementation "com.android.support:support-annotations:$rootProject.supportLibraryVersion" -// Removed Tue Apr 2 14:02:28 HKT 2019 after updating of dependencies and removing of redundant tests -// androidTestImplementation 'com.android.support.test:runner:1.0.2' -// androidTestImplementation 'com.android.support.test:rules:1.0.2' - - // Optional -- Hamcrest library - - //androidTestImplementation 'org.hamcrest:hamcrest-library:1.3' - - // Optional -- Robotium library - androidTestImplementation 'com.jayway.android.robotium:robotium-solo:5.5.4' - + //For androidx test support androidTestImplementation "androidx.test:core-ktx:1.2.0" androidTestImplementation "androidx.test.ext:junit-ktx:1.1.1" + androidTestImplementation "androidx.arch.core:core-testing:2.0.1" // Optional -- UI testing with Espresso -// androidTestImplementation 'com.android.support.test.e.presso:espresso-core:3.0.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0' - // For ViewModel lifecycle - implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' - - // Optional -- UI testing with UI Automator - - //androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v23:2.1.1' // https://mvnrepository.com/artifact/org.xwiki.platform/xwiki-platform-distribution-flavor-data-hsqldb implementation "org.xwiki.platform:xwiki-platform-distribution-flavor-data-hsqldb:11.4" + + implementation 'com.facebook.stetho:stetho:1.5.1' } diff --git a/app/src/androidTest/java/org/xwiki/android/sync/DB/AccountsDaoTest.kt b/app/src/androidTest/java/org/xwiki/android/sync/DB/AccountsDaoTest.kt new file mode 100755 index 00000000..19959930 --- /dev/null +++ b/app/src/androidTest/java/org/xwiki/android/sync/DB/AccountsDaoTest.kt @@ -0,0 +1,99 @@ +package org.xwiki.android.sync.DB + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.xwiki.android.sync.XWIKI_DEFAULT_SERVER_ADDRESS +import org.xwiki.android.sync.contactdb.AppDatabase +import org.xwiki.android.sync.contactdb.UserAccount +import org.xwiki.android.sync.contactdb.dao.AccountsDao +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class AccountsDaoTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + private lateinit var accountsDao: AccountsDao + private lateinit var db: AppDatabase + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + // Using an in-memory database because the information stored here disappears when the + // process is killed. + db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) + // Allowing main thread queries, just for testing. + .allowMainThreadQueries() + .build() + accountsDao = db.usersDao() + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + @Throws(Exception::class) + fun insertAndGetUser() = runBlocking { + val user = UserAccount( + "testUser1", + XWIKI_DEFAULT_SERVER_ADDRESS + ) + accountsDao.insertAccount(user) + val allUsers = accountsDao.getAllAccount() + assertEquals(allUsers[0].accountName, user.accountName) + } + + @Test + @Throws(Exception::class) + fun getAllUsers() = runBlocking { + val user1 = UserAccount( + "testUser1", + XWIKI_DEFAULT_SERVER_ADDRESS + ) + accountsDao.insertAccount(user1) + val user2 = UserAccount( + "testUser2", + XWIKI_DEFAULT_SERVER_ADDRESS + ) + accountsDao.insertAccount(user2) + val allUsers = accountsDao.getAllAccount() + assertEquals(allUsers[0].accountName, user1.accountName) + assertEquals(allUsers[1].accountName, user2.accountName) + } + + @Test + @Throws(Exception::class) + fun deleteAllUsers() = runBlocking { + val user1Id = accountsDao.insertAccount( + UserAccount( + "testUser1", + XWIKI_DEFAULT_SERVER_ADDRESS + ) + ) + val user2Id = accountsDao.insertAccount( + UserAccount( + "testUser2", + XWIKI_DEFAULT_SERVER_ADDRESS + ) + ) + accountsDao.deleteUser(user1Id) + accountsDao.deleteUser(user2Id) + val allUsers = accountsDao.getAllAccount() + assertTrue(allUsers.isEmpty()) + } +} diff --git a/app/src/androidTest/java/org/xwiki/android/sync/DB/LiveDataTestUtil.kt b/app/src/androidTest/java/org/xwiki/android/sync/DB/LiveDataTestUtil.kt new file mode 100755 index 00000000..e237ea94 --- /dev/null +++ b/app/src/androidTest/java/org/xwiki/android/sync/DB/LiveDataTestUtil.kt @@ -0,0 +1,44 @@ +package org.xwiki.android.sync.DB + +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +/** + * Get the value from a LiveData object. We're waiting for LiveData to emit, for 2 seconds. + * Once we got a notification via onChanged, we stop observing. + */ +@Throws(InterruptedException::class) +fun LiveData.waitForValue(): T { + val data = arrayOfNulls(1) + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(o: T?) { + data[0] = o + latch.countDown() + this@waitForValue.removeObserver(this) + } + } + this.observeForever(observer) + latch.await(2, TimeUnit.SECONDS) + + return data[0] as T +} diff --git a/app/src/androidTest/java/org/xwiki/android/sync/activities/SyncSettingsActivityTest.kt b/app/src/androidTest/java/org/xwiki/android/sync/activities/SyncSettingsActivityTest.kt index 8ee7f1f1..66c52d5c 100644 --- a/app/src/androidTest/java/org/xwiki/android/sync/activities/SyncSettingsActivityTest.kt +++ b/app/src/androidTest/java/org/xwiki/android/sync/activities/SyncSettingsActivityTest.kt @@ -1,21 +1,29 @@ package org.xwiki.android.sync.activities +import android.accounts.AccountManager +import android.content.Context +import android.content.Intent import androidx.lifecycle.LifecycleObserver +import androidx.room.Room import androidx.test.core.app.ActivityScenario -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.MediumTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.xwiki.android.sync.appContext -import android.content.Intent +import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.xwiki.android.sync.ACCOUNT_TYPE import org.xwiki.android.sync.R +import org.xwiki.android.sync.XWIKI_DEFAULT_SERVER_ADDRESS +import org.xwiki.android.sync.appContext +import org.xwiki.android.sync.contactdb.AppDatabase +import org.xwiki.android.sync.contactdb.UserAccount import org.xwiki.android.sync.utils.idlingResource @@ -30,8 +38,30 @@ open class SyncSettingsActivityTest : LifecycleObserver { @Before open fun setUp() { + val context = ApplicationProvider.getApplicationContext() + // Using an in-memory database because the information stored here disappears when the + // process is killed. + val db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) + // Allowing main thread queries, just for testing. + .allowMainThreadQueries() + .build() + val userDao = db.usersDao() + + val user = UserAccount( + "testUser1", + XWIKI_DEFAULT_SERVER_ADDRESS + ) + + userDao.insertAccount(user) + + IdlingRegistry.getInstance().register(idlingResource) - activityScenario = ActivityScenario.launch(SyncSettingsActivity::class.java) + val i = Intent(appContext, SyncSettingsActivity::class.java) + i.putExtra(AccountManager.KEY_ACCOUNT_NAME, user.accountName) + i.putExtra(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE) + i.putExtra("Test", true) + + activityScenario = ActivityScenario.launch(i) } @Test diff --git a/app/src/androidTest/java/org/xwiki/android/sync/auth/AuthenticatorActivityTest.kt b/app/src/androidTest/java/org/xwiki/android/sync/auth/AuthenticatorActivityTest.kt index 91bb691b..65c75ebd 100644 --- a/app/src/androidTest/java/org/xwiki/android/sync/auth/AuthenticatorActivityTest.kt +++ b/app/src/androidTest/java/org/xwiki/android/sync/auth/AuthenticatorActivityTest.kt @@ -7,19 +7,19 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onView -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Test -import org.junit.runner.RunWith -import org.xwiki.android.sync.ACCOUNT_TYPE -import org.xwiki.android.sync.AUTHTOKEN_TYPE_FULL_ACCESS -import org.xwiki.android.sync.appContext -import org.junit.Before import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions.* import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.xwiki.android.sync.ACCOUNT_TYPE +import org.xwiki.android.sync.AUTHTOKEN_TYPE_FULL_ACCESS import org.xwiki.android.sync.R +import org.xwiki.android.sync.appContext import org.xwiki.android.sync.utils.idlingResource /** diff --git a/app/src/androidTest/java/org/xwiki/android/sync/utils/StringUtilAndroidTest.kt b/app/src/androidTest/java/org/xwiki/android/sync/utils/StringUtilAndroidTest.kt deleted file mode 100644 index 02b29114..00000000 --- a/app/src/androidTest/java/org/xwiki/android/sync/utils/StringUtilAndroidTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.xwiki.android.sync.utils - -import androidx.test.runner.AndroidJUnit4 -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull - -/** - * StringUtilAndroidTest. - */ -@RunWith(AndroidJUnit4::class) -class StringUtilAndroidTest { - - @Before - @Throws(Exception::class) - fun setUp() { - - } - - @Test - @Throws(Exception::class) - fun iso8601ToDate() { - // this test in android runtime is different from that in java runtime - // for iso8601, Z or X ? Maybe java is standard. - assertNotNull(StringUtils.iso8601ToDate("2011-09-24T19:45:31+02:00")) - assertNotNull(StringUtils.iso8601ToDate("2016-05-20T13:11:48+0200")) - assertNotNull(StringUtils.iso8601ToDate("2016-05-20T13:11:48+02")) - assertNull(StringUtils.iso8601ToDate("2011-09-24T19:45:31")) - assertNull(StringUtils.iso8601ToDate("2011-092419:45:31")) - assertNull(StringUtils.iso8601ToDate("201")) - assertNull(StringUtils.iso8601ToDate("")) - } - -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 757bd568..40f4faaa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -64,6 +64,8 @@ + + +private val apiManagers: MutableMap = mutableMapOf() -/** - * Logging tag - */ -private const val TAG = "AppContext" +fun resolveApiManager(serverAddress: String, userAccountId: UserAccountId): BaseApiManager = apiManagers.getOrPut(userAccountId) { + BaseApiManager(serverAddress, userAccountId, userAccountsCookiesRepo) +} -/** - * Instance of context to use it in static methods - * @return known AppContext instance - */ +fun resolveApiManager(userAccount: UserAccount): BaseApiManager = resolveApiManager( + userAccount.serverAddress, userAccount.id +) -lateinit var appContext: Context - private set +private fun initRepos(context: AppContext) { + val appDatabase = AppDatabase.getInstance(context) + userAccountsCookiesRepo = SharedPreferencesUserAccountsCookiesRepository(context) + allUsersCacheRepository = DAOAllUsersCacheRepository( + appDatabase.allUsersCacheDao() + ) + groupsCacheRepository = DAOGroupsCacheRepository( + appDatabase.groupsCacheDao() + ) + userAccountsRepo = DAOUserAccountsRepository( + appDatabase.usersDao(), + groupsCacheRepository, + allUsersCacheRepository, + userAccountsCookiesRepo + ) +} /** - * @return actual base url + * Logging tag */ -fun currentBaseUrl(): String { - return validServerAddress(getValue(appContext, SERVER_ADDRESS, "localhost:8080")) -} +private const val TAG = "AppContext" + +val appCoroutineScope = CoroutineScope(Dispatchers.Default) /** * Add app as authorized @@ -93,24 +122,7 @@ fun isAuthorizedApp(packageName: String): Boolean { return packageList != null && packageList.contains(packageName) } -/** - * @return Current [.baseApiManager] value or create new and return - * - * @since 0.4 - */ -val apiManager : BaseApiManager - get() { - val url = currentBaseUrl() - val manager = try { - baseApiManager - } catch (e: UninitializedPropertyAccessException) { - baseApiManager = url to BaseApiManager(url) - baseApiManager - } - return manager.second - } - -open class AppContext : Application() { +class AppContext : Application() { /** * Set [.instance] to this object. @@ -118,6 +130,9 @@ open class AppContext : Application() { override fun onCreate() { super.onCreate() appContext = this + initRepos(this) + AccountManager.get(this).enableDetectingOfAccountsRemoving() Log.d(TAG, "on create") + Stetho.initializeWithDefaults(this) } } diff --git a/app/src/main/java/org/xwiki/android/sync/Constants.kt b/app/src/main/java/org/xwiki/android/sync/Constants.kt index 84fe2281..4502f5dd 100644 --- a/app/src/main/java/org/xwiki/android/sync/Constants.kt +++ b/app/src/main/java/org/xwiki/android/sync/Constants.kt @@ -30,6 +30,8 @@ const val ACCOUNT_TYPE = "org.xwiki.android.sync" const val ACCOUNT_NAME = "XWiki" const val USERDATA_SERVER = "XWIKI_SERVER" +const val XWIKI_DEFAULT_SERVER_ADDRESS = "https://www.xwiki.org/xwiki" + /** * Auth token types */ diff --git a/app/src/main/java/org/xwiki/android/sync/ViewModel/SyncSettingsViewModel.kt b/app/src/main/java/org/xwiki/android/sync/ViewModel/SyncSettingsViewModel.kt new file mode 100644 index 00000000..9728511c --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/ViewModel/SyncSettingsViewModel.kt @@ -0,0 +1,65 @@ +package org.xwiki.android.sync.ViewModel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.xwiki.android.sync.allUsersCacheRepository +import org.xwiki.android.sync.bean.ObjectSummary +import org.xwiki.android.sync.bean.XWikiGroup +import org.xwiki.android.sync.contactdb.* +import org.xwiki.android.sync.groupsCacheRepository +import org.xwiki.android.sync.userAccountsRepo + +class SyncSettingsViewModelFactory( + private val application: Application, + private val userAccountId: UserAccountId +) : ViewModelProvider.AndroidViewModelFactory(application) { + override fun create(modelClass: Class): T { + return if (modelClass == SyncSettingsViewModel::class.java) { + SyncSettingsViewModel(application, userAccountId) as T + } else { + super.create(modelClass) + } + } +} + +class SyncSettingsViewModel( + application: Application, + private val id: UserAccountId +) : AndroidViewModel(application) { + suspend fun getUser() : UserAccount? { + return userAccountsRepo.findByAccountId(id) + } + + fun updateUser(updatedUserAccount: UserAccount) { + viewModelScope.launch(Dispatchers.Default) { + if (updatedUserAccount.id == id) { + userAccountsRepo.updateAccount(updatedUserAccount) + } + } + } + + fun updateAllUsersCache(summaries: List) { + viewModelScope.launch(Dispatchers.Default) { + allUsersCacheRepository[id] = summaries + } + } + + fun getAllUsersCache(): List? { + return allUsersCacheRepository[id] + } + + fun updateGroupsCache(cache: List) { + viewModelScope.launch(Dispatchers.Default) { + groupsCacheRepository[id] = cache + } + } + + fun getGroupsCache(): List? { + return groupsCacheRepository[id] + } +} diff --git a/app/src/main/java/org/xwiki/android/sync/activities/AccountListAdapter.kt b/app/src/main/java/org/xwiki/android/sync/activities/AccountListAdapter.kt new file mode 100644 index 00000000..6595c9ca --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/activities/AccountListAdapter.kt @@ -0,0 +1,69 @@ +package org.xwiki.android.sync.activities + +import android.accounts.Account +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.LinearLayout +import android.widget.TextView +import org.xwiki.android.sync.R +import org.xwiki.android.sync.utils.AccountClickListener + +class AccountListAdapter ( + private val mContext: Context, + private var availableAccounts : Array, + private val listener : AccountClickListener +) : BaseAdapter() { + + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { + var view = convertView + val viewHolder: AccountListViewHolder + + view.let { + if (it == null) { + val inflater = LayoutInflater.from(mContext) + view = inflater.inflate(R.layout.account_list_layout, null) + viewHolder = AccountListViewHolder(view!!) + it?.tag = viewHolder + } else { + viewHolder = view?.tag as AccountListViewHolder + } + val account = getItem(position) + viewHolder.tvAccountName.text = account.name + viewHolder.tvAccountType.text = account.type + + viewHolder.llAccountItem.setOnClickListener { + listener(account) + } + } + + return view!! + } + + override fun getItem(position: Int): Account { + return availableAccounts[position] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getCount(): Int { + return availableAccounts.size + } + +} + +private class AccountListViewHolder (view: View) { + val tvAccountName : TextView + val tvAccountType : TextView + val llAccountItem : LinearLayout + + init { + tvAccountName = view.findViewById(R.id.tvAccountName) + tvAccountType = view.findViewById(R.id.tvAccountType) + llAccountItem = view.findViewById(R.id.llAccountItem) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xwiki/android/sync/activities/EditContact/EditContactActivity.kt b/app/src/main/java/org/xwiki/android/sync/activities/EditContact/EditContactActivity.kt index aa7bd51f..3d3cce79 100644 --- a/app/src/main/java/org/xwiki/android/sync/activities/EditContact/EditContactActivity.kt +++ b/app/src/main/java/org/xwiki/android/sync/activities/EditContact/EditContactActivity.kt @@ -7,16 +7,19 @@ import android.widget.EditText import androidx.core.view.get import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.xwiki.android.sync.R import org.xwiki.android.sync.activities.base.BaseActivity -import org.xwiki.android.sync.apiManager import org.xwiki.android.sync.bean.MutableInternalXWikiUserInfo import org.xwiki.android.sync.bean.XWikiUserFull import org.xwiki.android.sync.contactdb.* import org.xwiki.android.sync.rest.XWikiHttp +import org.xwiki.android.sync.appCoroutineScope +import org.xwiki.android.sync.resolveApiManager +import org.xwiki.android.sync.rest.BaseApiManager +import org.xwiki.android.sync.userAccountsRepo import org.xwiki.android.sync.utils.StringUtils.isEmail import org.xwiki.android.sync.utils.StringUtils.isEmpty import org.xwiki.android.sync.utils.StringUtils.isPhone @@ -36,13 +39,6 @@ private const val reloginTryes = 3 */ class EditContactActivity : BaseActivity() { - /** - * The scope of edit contact activity - * - * @since 0.6 - */ - private val scope = CoroutineScope(Dispatchers.Default) - /** * Lazy initialized contact row id * @@ -86,6 +82,8 @@ class EditContactActivity : BaseActivity() { } } + private var apiManager: BaseApiManager? = null + /** * Lazy initialized splitted {@link #userId} */ @@ -159,12 +157,26 @@ class EditContactActivity : BaseActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_edit_contact) + accountName ?.let { accountName -> + appCoroutineScope.launch { + val account = userAccountsRepo.findByAccountName(accountName) ?:let { + apiManager = null + return@launch // TODO:: HERE MUST BE CORRECT CLOSING OF ACTIVITY + } + apiManager = resolveApiManager( + account + ) + + withContext(Dispatchers.Main) { + refillData() + } + } + } + findViewById(R.id.editContactSaveButton).setOnClickListener { view -> saveData(view) } - - refillData() } /** @@ -194,7 +206,7 @@ class EditContactActivity : BaseActivity() { } disableContainer() - apiManager.xwikiServicesApi.updateUser( + apiManager ?.xwikiServicesApi ?.updateUser( it.wiki, it.space, it.pageName, @@ -205,11 +217,11 @@ class EditContactActivity : BaseActivity() { it.address, it.company, it.comment - ).observeOn( + ) ?.observeOn( AndroidSchedulers.mainThread() - ).subscribeOn( + ) ?.subscribeOn( Schedulers.newThread() - ).subscribe( + ) ?.subscribe( { Snackbar.make( view, @@ -220,7 +232,7 @@ class EditContactActivity : BaseActivity() { user -> updateUserInDatabase(user) - scope.launch (Dispatchers.Main) { + appCoroutineScope.launch (Dispatchers.Main) { Snackbar.make( view, getString(R.string.success), @@ -235,9 +247,8 @@ class EditContactActivity : BaseActivity() { } ) { if (it?.unauthorized == true) { - XWikiHttp.relogin( - this@EditContactActivity, - accountName + apiManager ?.xWikiHttp ?.relogin( + this@EditContactActivity )?.subscribe( { saveData(view, count + 1) @@ -319,7 +330,7 @@ class EditContactActivity : BaseActivity() { rowId ?: return, splittedUserId ?: return ).also { - scope.launch (Dispatchers.Main) { + appCoroutineScope.launch (Dispatchers.Main) { firstNameEditText.text.apply { clear() insert(0, it.firstName ?: "") @@ -357,7 +368,7 @@ class EditContactActivity : BaseActivity() { * Disable all possible to add data (prohibit input) */ private fun disableContainer() { - scope.launch (Dispatchers.Main) { + appCoroutineScope.launch (Dispatchers.Main) { container.isEnabled = false (0 until container.childCount).forEach { container[it].isEnabled = false @@ -369,7 +380,7 @@ class EditContactActivity : BaseActivity() { * Enable all possible to add data (permit input) */ private fun enableContainer() { - scope.launch (Dispatchers.Main) { + appCoroutineScope.launch (Dispatchers.Main) { container.isEnabled = true (0 until container.childCount).forEach { container[it].isEnabled = true @@ -402,7 +413,7 @@ class EditContactActivity : BaseActivity() { * @see updateUserInDatabase */ private fun manuallyUpdateUserInfo(view: View) { - scope.launch (Dispatchers.Main) { + appCoroutineScope.launch (Dispatchers.Main) { Snackbar.make( view, getString(R.string.syncContactInfoWithServer), @@ -410,16 +421,16 @@ class EditContactActivity : BaseActivity() { ).show() } splittedUserId ?.let { - apiManager.xwikiServicesApi.getFullUserDetails( + apiManager ?.xwikiServicesApi ?.getFullUserDetails( it[0], it[1], it[2] - ).subscribeOn( + ) ?.subscribeOn( Schedulers.newThread() - ).subscribe( + ) ?.subscribe( object : Observer { override fun onError(e: Throwable?) { - scope.launch (Dispatchers.Main) { + appCoroutineScope.launch (Dispatchers.Main) { Snackbar.make( view, e ?. message ?: getString(R.string.cantSyncContact), @@ -432,7 +443,7 @@ class EditContactActivity : BaseActivity() { t ?.let { updateUserInDatabase(it) - scope.launch (Dispatchers.Main) { + appCoroutineScope.launch (Dispatchers.Main) { Snackbar.make( view, getString(R.string.success), diff --git a/app/src/main/java/org/xwiki/android/sync/activities/GrantPermissionActivity.kt b/app/src/main/java/org/xwiki/android/sync/activities/GrantPermissionActivity.kt index 797a33dc..89b41107 100644 --- a/app/src/main/java/org/xwiki/android/sync/activities/GrantPermissionActivity.kt +++ b/app/src/main/java/org/xwiki/android/sync/activities/GrantPermissionActivity.kt @@ -29,9 +29,11 @@ import android.view.View import android.widget.TextView import android.widget.Toast import androidx.appcompat.widget.Toolbar +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.xwiki.android.sync.* import org.xwiki.android.sync.auth.KEY_AUTH_TOKEN_TYPE -import org.xwiki.android.sync.utils.getValue /** * A grant permission activity. @@ -83,20 +85,24 @@ class GrantPermissionActivity : AccountAuthenticatorActivity() { addAuthorizedApp(packageName) val mAccountManager = AccountManager.get(applicationContext) val account = Account(accountName, ACCOUNT_TYPE) - val authToken = getValue( - appContext - .applicationContext - , COOKIE, - null) - mAccountManager.setAuthToken(account, authTokenType, authToken) - val intent = Intent() - intent.putExtra(AccountManager.KEY_AUTHTOKEN, authToken) - intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountType) - intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, accountName) - intent.putExtra(SERVER_ADDRESS, currentBaseUrl()) - setAccountAuthenticatorResult(intent.extras) - setResult(Activity.RESULT_OK, intent) - finish() + appCoroutineScope.launch { + val userAccount = userAccountsRepo.findByAccountName(accountName.toString()) ?: return@launch + val accountServerUrl = userAccount.serverAddress + val id = userAccount.id + val authToken = userAccountsCookiesRepo[id] + + withContext(Dispatchers.Main) { + mAccountManager.setAuthToken(account, authTokenType, authToken) + val intent = Intent() + intent.putExtra(AccountManager.KEY_AUTHTOKEN, authToken) + intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountType) + intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, accountName) + intent.putExtra(SERVER_ADDRESS, accountServerUrl) + setAccountAuthenticatorResult(intent.extras) + setResult(Activity.RESULT_OK, intent) + finish() + } + } } } diff --git a/app/src/main/java/org/xwiki/android/sync/activities/GroupListAdapter.kt b/app/src/main/java/org/xwiki/android/sync/activities/GroupListAdapter.kt index b090fb76..29701b8c 100644 --- a/app/src/main/java/org/xwiki/android/sync/activities/GroupListAdapter.kt +++ b/app/src/main/java/org/xwiki/android/sync/activities/GroupListAdapter.kt @@ -19,20 +19,19 @@ */ package org.xwiki.android.sync.activities -import android.content.Context +import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.BaseAdapter import android.widget.CheckBox +import android.widget.RelativeLayout import android.widget.TextView +import androidx.annotation.Nullable +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView import org.xwiki.android.sync.R -import org.xwiki.android.sync.SELECTED_GROUPS import org.xwiki.android.sync.bean.XWikiGroup -import org.xwiki.android.sync.utils.getArrayList -import org.xwiki.android.sync.utils.putArrayList - -import java.util.ArrayList +import org.xwiki.android.sync.utils.GroupsListChangeListener /** * [android.widget.Adapter] which can be used to show groups. @@ -47,8 +46,10 @@ import java.util.ArrayList * @param groupList Initial group list */ -class GroupListAdapter(private val mContext: Context, private var groupList: List) - : BaseAdapter() { +class GroupListAdapter( + private var groupList: List, + private var groupsListChangeListener: GroupsListChangeListener +) : RecyclerView.Adapter() { /** * List of selected items. @@ -61,80 +62,49 @@ class GroupListAdapter(private val mContext: Context, private var groupList: Lis val selectGroups: List get() = selected - /** - * @return Count of items - */ - override fun getCount(): Int { - return groupList.size - } - /** - * @param position Position of object (must be 0 <= position < [.getCount] - * @return Object ([XWikiGroup] if be exactly) - */ - override fun getItem(position: Int): XWikiGroup { - return groupList[position] + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val itemView = LayoutInflater.from(parent.context).inflate(R.layout.list_item_group, parent, false) + return ViewHolder(itemView) } - /** - * @param position Position of object (must be 0 <= position < [.getCount] - * @return position - */ - override fun getItemId(position: Int): Long { - return position.toLong() + override fun getItemCount(): Int { + return groupList.size } - /** - * Create and set up view. - * - * @param position Position of object (must be 0 <= position < [.getCount] - * @param convertView Old [View] - * @param parent Parent view where result will be placed - * @return Result [View] - */ - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - var convertView = convertView - val viewHolder: ViewHolder - if (convertView == null) { - val inflater = LayoutInflater.from(mContext) - convertView = inflater.inflate(R.layout.list_item_group, null) - viewHolder = ViewHolder(convertView!!) - convertView.tag = viewHolder - } else { - viewHolder = convertView.tag as ViewHolder - } - - val group = getItem(position) - viewHolder.groupNameTextView.text = group.pageName - viewHolder.lastModifiedTime.text = group.lastModifiedDate.substring(0, 10) - viewHolder.versionTextView.text = group.wiki - viewHolder.checkBox.isChecked = selected.contains(group) + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + viewHolder.groupNameTextView.text = groupList[position].pageName + viewHolder.lastModifiedTime.text = groupList[position].lastModifiedDate.substring(0, 10) + viewHolder.versionTextView.text = groupList[position].wiki + viewHolder.checkBox.isChecked = selected.contains(groupList[position]) - convertView.setOnClickListener { + viewHolder.contactContent.setOnClickListener { if (viewHolder.checkBox.isChecked) { viewHolder.checkBox.isChecked = false - selected.remove(group) + selected.remove(groupList[position]) } else { viewHolder.checkBox.isChecked = true - selected.add(group) + selected.add(groupList[position]) } + groupsListChangeListener.onChangeListener() } + viewHolder.checkBox.setOnClickListener { if (viewHolder.checkBox.isChecked) { - selected.add(group) + selected.add(groupList[position]) } else { - selected.remove(group) + selected.remove(groupList[position]) } + groupsListChangeListener.onChangeListener() } - return convertView } /** * Init groups which was selected in previous time. */ - private fun initSelectedGroup() { - val groupIds = getArrayList(mContext, SELECTED_GROUPS) + private fun initSelectedGroup(selectedGroups: MutableList?) { + val groupIds = selectedGroups if (groupIds == null || groupIds.size == 0) { return } @@ -149,14 +119,14 @@ class GroupListAdapter(private val mContext: Context, private var groupList: Lis /** * Save current state of selected groups for future use */ - fun saveSelectedGroups() { + fun saveSelectedGroups(): MutableList { val selectedStrings = ArrayList() for (group in selected) { selectedStrings.add(group.id) } - putArrayList(mContext, SELECTED_GROUPS, selectedStrings) + return selectedStrings } /** @@ -164,29 +134,62 @@ class GroupListAdapter(private val mContext: Context, private var groupList: Lis * * @param groups new list */ - fun refresh(groups: List) { - if (groupList != null && groupList != groups) { - groupList = groups - } - initSelectedGroup() - notifyDataSetChanged() + fun refresh(groups: List, selectedGroups: MutableList?) { + val diffResult = DiffUtil.calculateDiff(GroupListDiffUtilCallBack(groups, groupList)) + diffResult.dispatchUpdatesTo(this) + groupList = listOf() + this.groupList = groups + initSelectedGroup(selectedGroups) + groupsListChangeListener.onChangeListener() } /** * Help view holder class. */ - private class ViewHolder(view: View) { + class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { val groupNameTextView: TextView val lastModifiedTime: TextView val versionTextView: TextView val checkBox: CheckBox + val contactContent : RelativeLayout init { - groupNameTextView = view.findViewById(R.id.groupName) - lastModifiedTime = view.findViewById(R.id.lastModifiedTime) - versionTextView = view.findViewById(R.id.version) - checkBox = view.findViewById(R.id.checkbox) + groupNameTextView = itemView.findViewById(R.id.groupName) + lastModifiedTime = itemView.findViewById(R.id.lastModifiedTime) + versionTextView = itemView.findViewById(R.id.version) + checkBox = itemView.findViewById(R.id.checkbox) + contactContent = itemView.findViewById(R.id.contact_content) } } + class GroupListDiffUtilCallBack(internal var newList: List, internal var oldList: List) : + DiffUtil.Callback() { + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return newList[newItemPosition].id.equals(oldList[oldItemPosition].id) + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return newList[newItemPosition].id.equals(oldList[oldItemPosition].id) + } + + @Nullable + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { + val newModel = newList[newItemPosition] + val oldModel = oldList[oldItemPosition] + + val diff = Bundle() + + if (newModel.id !== oldModel.id) { + diff.putString("guid", newModel.id) + } + return if (diff.size() == 0) { + null + } else diff + } + } } diff --git a/app/src/main/java/org/xwiki/android/sync/activities/SelectAccountActivity.kt b/app/src/main/java/org/xwiki/android/sync/activities/SelectAccountActivity.kt new file mode 100644 index 00000000..48f194d5 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/activities/SelectAccountActivity.kt @@ -0,0 +1,37 @@ +package org.xwiki.android.sync.activities + +import android.accounts.Account +import android.accounts.AccountManager +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import androidx.databinding.DataBindingUtil +import org.xwiki.android.sync.ACCOUNT_TYPE +import org.xwiki.android.sync.R +import org.xwiki.android.sync.activities.base.BaseActivity +import org.xwiki.android.sync.databinding.ActSelectAccountBinding +import org.xwiki.android.sync.utils.AccountClickListener + +class SelectAccountActivity : BaseActivity(), AccountClickListener { + + lateinit var binding : ActSelectAccountBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.act_select_account) + + val mAccountManager = AccountManager.get(applicationContext) + val availableAccountsList = mAccountManager.getAccountsByType(ACCOUNT_TYPE) + + val adapter = AccountListAdapter(this, availableAccountsList, this) + binding.lvAvailableAccounts.adapter = adapter + } + + override fun invoke(selectedAccount: Account) { + val intent = Intent() + intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, selectedAccount.name) + intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, selectedAccount.type) + setResult(Activity.RESULT_OK, intent) + finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xwiki/android/sync/activities/SettingServerIpViewFlipper.kt b/app/src/main/java/org/xwiki/android/sync/activities/SettingServerIpViewFlipper.kt index fe9831f3..858b5ba4 100644 --- a/app/src/main/java/org/xwiki/android/sync/activities/SettingServerIpViewFlipper.kt +++ b/app/src/main/java/org/xwiki/android/sync/activities/SettingServerIpViewFlipper.kt @@ -24,10 +24,7 @@ import android.util.Log import android.view.View import android.widget.EditText import org.xwiki.android.sync.R -import org.xwiki.android.sync.SERVER_ADDRESS import org.xwiki.android.sync.auth.AuthenticatorActivity -import org.xwiki.android.sync.utils.putValue - import java.net.MalformedURLException import java.net.URL @@ -50,9 +47,10 @@ class SettingServerIpViewFlipper(activity: AuthenticatorActivity, contentRootVie * Check typed server address and call sign in if all is ok. */ override fun doNext() { - val serverAddress = checkInput() - if (serverAddress != null) { - putValue(mContext, SERVER_ADDRESS, serverAddress) + checkInput().let { + if (it != null) { + mActivity.serverUrl = it + } } } diff --git a/app/src/main/java/org/xwiki/android/sync/activities/SignInViewFlipper.kt b/app/src/main/java/org/xwiki/android/sync/activities/SignInViewFlipper.kt index 6b7b3b1d..a4ba0eee 100644 --- a/app/src/main/java/org/xwiki/android/sync/activities/SignInViewFlipper.kt +++ b/app/src/main/java/org/xwiki/android/sync/activities/SignInViewFlipper.kt @@ -26,16 +26,19 @@ import android.os.Handler import android.text.TextUtils import android.view.View import androidx.databinding.DataBindingUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import org.xwiki.android.sync.R -import org.xwiki.android.sync.SERVER_ADDRESS +import org.xwiki.android.sync.appCoroutineScope import org.xwiki.android.sync.auth.AuthenticatorActivity import org.xwiki.android.sync.auth.PARAM_USER_PASS -import org.xwiki.android.sync.auth.PARAM_USER_SERVER -import org.xwiki.android.sync.rest.XWikiHttp +import org.xwiki.android.sync.contactdb.UserAccount +import org.xwiki.android.sync.contactdb.abstracts.deleteAccount +import org.xwiki.android.sync.resolveApiManager +import org.xwiki.android.sync.userAccountsRepo import org.xwiki.android.sync.utils.decrement -import org.xwiki.android.sync.utils.getValue import org.xwiki.android.sync.utils.increment -import rx.Subscription import rx.android.schedulers.AndroidSchedulers /** @@ -79,10 +82,12 @@ class SignInViewFlipper(activity: AuthenticatorActivity, contentRootView: View) binding.signInButton.setOnClickListener { if (checkInput()) { increment() + val signInJob = submit() mActivity.showProgress( - mContext.getText(R.string.sign_in_authenticating), - submit() - ) + mContext.getText(R.string.sign_in_authenticating) + ) { + signInJob.cancel() + } } } } @@ -139,33 +144,50 @@ class SignInViewFlipper(activity: AuthenticatorActivity, contentRootView: View) * * @return Subscription which can be unsubscribed for preventing log in if user cancel it */ - private fun submit(): Subscription { + private fun submit(): Job { val userName = accountName val userPass = accountPassword - return XWikiHttp.login( - userName, - userPass - ) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { authtoken -> - mActivity.hideProgress() - if (authtoken == null) { + return appCoroutineScope.launch { + val user = userAccountsRepo.createAccount( + UserAccount( + accountName, + mActivity.serverUrl ?: return@launch + ) + ) ?: let { + // Something went wrong, because we did not get user account + return@launch + } + + val apiManager = resolveApiManager(user) + apiManager.xWikiHttp.login( + userName, + userPass + ) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { authtoken -> + mActivity.hideProgress() + if (authtoken == null) { + showErrorMessage(mContext.getString(R.string.loginError)) + } else { + signedIn( + authtoken, + userName, + userPass + ) + } + }, + { + mActivity.hideProgress() showErrorMessage(mContext.getString(R.string.loginError)) - } else { - signedIn( - authtoken, - userName, - userPass - ) + + launch { + userAccountsRepo.deleteAccount(user) + } } - }, - { - mActivity.hideProgress() - showErrorMessage(mContext.getString(R.string.loginError)) - } - ) + ) + } } /** @@ -183,15 +205,12 @@ class SignInViewFlipper(activity: AuthenticatorActivity, contentRootView: View) username: String, password: String ): Intent { - val userServer = getValue(mContext, SERVER_ADDRESS, null) - val accountType = mActivity.intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE) val data = Bundle() data.putString(AccountManager.KEY_ACCOUNT_NAME, username) data.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType) data.putString(AccountManager.KEY_AUTHTOKEN, authtoken) - data.putString(PARAM_USER_SERVER, userServer) data.putString(PARAM_USER_PASS, password) val intent = Intent() @@ -219,7 +238,7 @@ class SignInViewFlipper(activity: AuthenticatorActivity, contentRootView: View) password ) - mActivity.runOnUiThread { + appCoroutineScope.launch(Dispatchers.Main) { mActivity.hideProgress() mActivity.finishLogin(signedIn) mActivity.hideInputMethod() diff --git a/app/src/main/java/org/xwiki/android/sync/activities/SyncSettingsActivity.kt b/app/src/main/java/org/xwiki/android/sync/activities/SyncSettingsActivity.kt index ed8da458..cbbee972 100644 --- a/app/src/main/java/org/xwiki/android/sync/activities/SyncSettingsActivity.kt +++ b/app/src/main/java/org/xwiki/android/sync/activities/SyncSettingsActivity.kt @@ -1,6 +1,8 @@ package org.xwiki.android.sync.activities +import android.accounts.Account import android.accounts.AccountManager +import android.app.Activity import android.content.ComponentName import android.content.ContentResolver import android.content.Context @@ -9,21 +11,35 @@ import android.net.Uri import android.os.Bundle import android.provider.ContactsContract import android.view.View -import android.widget.* +import android.widget.AdapterView +import android.widget.Toast import androidx.databinding.DataBindingUtil +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.xwiki.android.sync.* +import org.xwiki.android.sync.ViewModel.SyncSettingsViewModel +import org.xwiki.android.sync.ViewModel.SyncSettingsViewModelFactory import org.xwiki.android.sync.activities.base.BaseActivity import org.xwiki.android.sync.bean.ObjectSummary import org.xwiki.android.sync.bean.SerachResults.CustomObjectsSummariesContainer import org.xwiki.android.sync.bean.SerachResults.CustomSearchResultContainer import org.xwiki.android.sync.bean.XWikiGroup +import org.xwiki.android.sync.contactdb.UserAccount +import org.xwiki.android.sync.contactdb.clearOldAccountContacts +import org.xwiki.android.sync.databinding.ActivitySyncSettingsBinding +import org.xwiki.android.sync.rest.BaseApiManager +import org.xwiki.android.sync.utils.GroupsListChangeListener +import org.xwiki.android.sync.utils.decrement +import org.xwiki.android.sync.utils.getAppVersionName +import org.xwiki.android.sync.utils.increment import rx.android.schedulers.AndroidSchedulers import rx.functions.Action1 import rx.schedulers.Schedulers -import java.util.ArrayList -import org.xwiki.android.sync.contactdb.clearOldAccountContacts -import org.xwiki.android.sync.databinding.ActivitySyncSettingsBinding -import org.xwiki.android.sync.utils.* +import java.util.* /** @@ -66,7 +82,7 @@ private fun openAppMarket(context: Context) { } } -class SyncSettingsActivity : BaseActivity() { +class SyncSettingsActivity : BaseActivity(), GroupsListChangeListener { /** * DataBinding for accessing layout variables. @@ -86,17 +102,17 @@ class SyncSettingsActivity : BaseActivity() { /** * List of received groups. */ - private lateinit var groups: MutableList + private val groups: MutableList = mutableListOf() /** * List of received all users. */ - private lateinit var allUsers: MutableList + private val allUsers: MutableList = mutableListOf() /** * Currently chosen sync type. */ - private var chosenSyncType = SYNC_TYPE_NO_NEED_SYNC + private var chosenSyncType: Int? = SYNC_TYPE_NO_NEED_SYNC /** * Flag of currently loading groups. @@ -110,6 +126,20 @@ class SyncSettingsActivity : BaseActivity() { @Volatile private var allUsersAreLoading: Boolean = false + private lateinit var currentUserAccountName : String + + private lateinit var currentUserAccountType : String + + private lateinit var userAccount : UserAccount + + private lateinit var syncSettingsViewModel: SyncSettingsViewModel + + private lateinit var apiManager: BaseApiManager + + private var selectedStrings = ArrayList() + + private lateinit var context: LifecycleOwner + /** * Init all views and other activity objects * @@ -119,35 +149,66 @@ class SyncSettingsActivity : BaseActivity() { */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = DataBindingUtil.setContentView(this, R.layout.activity_sync_settings); + + context = this + binding = DataBindingUtil.setContentView(this, R.layout.activity_sync_settings) binding.versionCheck.text = String.format( getString(R.string.versionTemplate), getAppVersionName(this) ) - binding.versionCheck.setOnClickListener { v -> openAppMarket(v.context) } - binding.listView.emptyView = binding.syncTypeGetErrorContainer - groups = ArrayList() - allUsers = ArrayList() - mGroupAdapter = GroupListAdapter(this, groups) - mUsersAdapter = UserListAdapter(this, allUsers) - initData() - binding.listView.choiceMode = ListView.CHOICE_MODE_MULTIPLE + if (intent.extras != null && intent.extras.get("account") != null) { + val intentAccount : Account = intent.extras.get("account") as Account + currentUserAccountName = intentAccount.name + currentUserAccountType = intentAccount.type + } else { + currentUserAccountName = intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) + currentUserAccountType = intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE) + } + + mGroupAdapter = GroupListAdapter(groups, this) + mUsersAdapter = UserListAdapter(allUsers) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = mUsersAdapter binding.selectSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { chosenSyncType = position - updateListView() + updateListView(false) } - override fun onNothingSelected(parent: AdapterView<*>) { + override fun onNothingSelected(parent: AdapterView<*>) {} + } + binding.rvChangeSelectedAccount.setOnClickListener { + val intent : Intent = Intent (this, SelectAccountActivity::class.java) + startActivityForResult(intent, 1000) + } + binding.btTryAgain.setOnClickListener { + initData() + } + binding.versionCheck.setOnClickListener { v -> openAppMarket(v.context) } + + binding.nextButton.setOnClickListener { syncSettingComplete(it) } + initData() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, intent) + if (requestCode == 1000) { + if (resultCode == Activity.RESULT_OK) { + if (data != null) { + currentUserAccountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) + currentUserAccountType = data.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE) + + binding.tvSelectedSyncAcc.text = currentUserAccountName + binding.tvSelectedSyncType.text = currentUserAccountType + initData() + } } } - chosenSyncType = getValue(this, SYNC_TYPE, SYNC_TYPE_ALL_USERS) - binding.selectSpinner.setSelection(chosenSyncType) } /** @@ -168,14 +229,71 @@ class SyncSettingsActivity : BaseActivity() { } } + private fun showProgressBar() { + runOnUiThread { + binding.listViewProgressBar.visibility = View.VISIBLE + binding.settingsSyncListViewContainer.visibility = View.GONE + } + } + + private fun hideProgressBar() { + runOnUiThread { + binding.listViewProgressBar.visibility = View.GONE + binding.settingsSyncListViewContainer.visibility = View.VISIBLE + } + } + /** * Load data to groups and all users lists. * * @since 0.4 */ - fun initData() { - increment() - if (groups.isEmpty()) { + private fun initData() { + binding.tvSelectedSyncAcc.text = currentUserAccountName + binding.tvSelectedSyncType.text = currentUserAccountType + + if (!intent.getBooleanExtra("Test", false)) { + showProgressBar() + } + + appCoroutineScope.launch { + userAccount = userAccountsRepo.findByAccountName(currentUserAccountName) ?: return@launch + chosenSyncType = userAccount.syncType + apiManager = resolveApiManager(userAccount) + + selectedStrings.clear() + selectedStrings = userAccount.selectedGroupsList as ArrayList + + withContext(Dispatchers.Main) { + userAccount.syncType.let { + if (it >= 0) { + chosenSyncType = it + binding.selectSpinner.setSelection(it) + } + } + syncSettingsViewModel = ViewModelProviders.of( + this@SyncSettingsActivity, + SyncSettingsViewModelFactory(application, userAccount.id) + ).get(SyncSettingsViewModel::class.java) + + chosenSyncType = userAccount.syncType + chosenSyncType?.let { binding.selectSpinner.setSelection(it) } + } + + updateSyncList() + } + } + + private fun updateSyncList () { + updateSyncGroups() + updateSyncAllUsers() + } + + private fun updateSyncGroups() { + val groupsCache = syncSettingsViewModel.getGroupsCache() ?: emptyList() + + if (groupsCache.isEmpty()) { + increment() groupsAreLoading = true apiManager.xwikiServicesApi.availableGroups( LIMIT_MAX_SYNC_USERS @@ -185,11 +303,15 @@ class SyncSettingsActivity : BaseActivity() { .subscribe( Action1> { xWikiGroupCustomSearchResultContainer -> groupsAreLoading = false + runOnUiThread { + binding.syncTypeGetErrorContainer.visibility = View.GONE + } val searchResults = xWikiGroupCustomSearchResultContainer.searchResults if (searchResults != null) { groups.clear() groups.addAll(searchResults) - updateListView() + syncSettingsViewModel.updateGroupsCache(searchResults) + updateListView(false) } }, Action1 { @@ -200,23 +322,39 @@ class SyncSettingsActivity : BaseActivity() { R.string.cantGetGroups, Toast.LENGTH_SHORT ).show() + binding.syncTypeGetErrorContainer.visibility = View.VISIBLE } - refreshProgressBar() + hideProgressBar() decrement() } ) + } else { + groups.clear() + groups.addAll(groupsCache) + updateListView(false) } - if (allUsers.isEmpty()) { + } + + private fun updateSyncAllUsers() { + val users = syncSettingsViewModel.getAllUsersCache() ?: emptyList() + if (users.isEmpty()) { allUsersAreLoading = true apiManager.xwikiServicesApi.allUsersPreview .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( Action1> { summaries -> + runOnUiThread { + binding.syncTypeGetErrorContainer.visibility = View.GONE + } allUsersAreLoading = false allUsers.clear() allUsers.addAll(summaries.objectSummaries) - updateListView() + + syncSettingsViewModel.updateAllUsersCache( + summaries.objectSummaries + ) + updateListView(true) decrement() }, Action1 { @@ -227,14 +365,16 @@ class SyncSettingsActivity : BaseActivity() { R.string.cantGetAllUsers, Toast.LENGTH_SHORT ).show() + binding.syncTypeGetErrorContainer.visibility = View.VISIBLE } - refreshProgressBar() + hideProgressBar() decrement() } ) - } - if (allUsersAreLoading || groupsAreLoading) { - refreshProgressBar() + } else { + allUsers.clear() + allUsers.addAll(users) + updateListView(false) } } @@ -262,24 +402,25 @@ class SyncSettingsActivity : BaseActivity() { /** * Update list view and hide/show view from [.getListViewContainer] */ - private fun updateListView() { - if (syncNothing()) { - binding.settingsSyncListViewContainer.visibility = View.GONE - binding.listViewProgressBar.visibility = View.GONE - } else { - binding.settingsSyncListViewContainer.visibility = View.VISIBLE - val adapter: BaseAdapter? - if (syncGroups()) { - adapter = mGroupAdapter - mGroupAdapter.refresh(groups) + private fun updateListView(hideProgressBar: Boolean) { + appCoroutineScope.launch(Dispatchers.Main) { + if (syncNothing()) { + binding.settingsSyncListViewContainer.visibility = View.GONE + binding.listViewProgressBar.visibility = View.GONE } else { - adapter = mUsersAdapter + binding.settingsSyncListViewContainer.visibility = View.VISIBLE + if (syncGroups()) { + binding.recyclerView.adapter = mGroupAdapter + mGroupAdapter.refresh(groups, userAccount.selectedGroupsList) + } else { + binding.recyclerView.adapter = mUsersAdapter + mUsersAdapter.refresh(allUsers) + } mUsersAdapter.refresh(allUsers) + if (hideProgressBar) { + hideProgressBar() + } } - if (adapter !== binding.listView.adapter) { - binding.listView.adapter = adapter - } - refreshProgressBar() } } @@ -287,16 +428,19 @@ class SyncSettingsActivity : BaseActivity() { * Save settings of synchronization. */ fun syncSettingComplete(v: View) { - //check changes. if no change, directly return - val oldSyncType = getValue(this, SYNC_TYPE, -1) + val oldSyncType = userAccount.syncType if (oldSyncType == chosenSyncType && !syncGroups()) { return } - //TODO:: fix when will separate to different accounts val mAccountManager = AccountManager.get(applicationContext) val availableAccounts = mAccountManager.getAccountsByType(ACCOUNT_TYPE) - val account = availableAccounts[0] + var account : Account = availableAccounts[0] + for (acc in availableAccounts) { + if (acc.name.equals(currentUserAccountName)) { + account = acc + } + } clearOldAccountContacts( contentResolver, @@ -305,23 +449,31 @@ class SyncSettingsActivity : BaseActivity() { //if has changes, set sync if (syncNothing()) { - putValue(applicationContext, SYNC_TYPE, SYNC_TYPE_NO_NEED_SYNC) + userAccount.syncType = SYNC_TYPE_NO_NEED_SYNC + userAccount.let { syncSettingsViewModel.updateUser(it) } setSync(false) + finish() } else if (syncAllUsers()) { - putValue(applicationContext, SYNC_TYPE, SYNC_TYPE_ALL_USERS) + userAccount.syncType = SYNC_TYPE_ALL_USERS + userAccount.let { syncSettingsViewModel.updateUser(it) } setSync(true) + finish() } else if (syncGroups()) { //compare to see if there are some changes. if (oldSyncType == chosenSyncType && compareSelectGroups()) { - Toast.makeText(this, getString(R.string.unchangedSettings), Toast.LENGTH_LONG).show() return } - mGroupAdapter.saveSelectedGroups() + userAccount.selectedGroupsList.clear() + userAccount.selectedGroupsList.addAll(mGroupAdapter.saveSelectedGroups()) - putValue(applicationContext, SYNC_TYPE, SYNC_TYPE_SELECTED_GROUPS) - setSync(true) - finish() // TODO:: FIX IT TO CORRECT HANDLE OF COMPLETING SETTINGS + userAccount.syncType = SYNC_TYPE_SELECTED_GROUPS + + appCoroutineScope.launch { + userAccountsRepo.updateAccount(userAccount) + setSync(true) + finish() + } } } @@ -333,7 +485,12 @@ class SyncSettingsActivity : BaseActivity() { private fun setSync(syncEnabled: Boolean) { val mAccountManager = AccountManager.get(applicationContext) val availableAccounts = mAccountManager.getAccountsByType(ACCOUNT_TYPE) - val account = availableAccounts[0] + var account : Account = availableAccounts[0] + for (acc in availableAccounts) { + if (acc.name.equals(currentUserAccountName)) { + account = acc + } + } if (syncEnabled) { mAccountManager.setUserData(account, SYNC_MARKER_KEY, null) ContentResolver.cancelSync(account, ContactsContract.AUTHORITY) @@ -359,22 +516,29 @@ class SyncSettingsActivity : BaseActivity() { //new val newList = mGroupAdapter.selectGroups //old - val oldList = getArrayList(applicationContext, SELECTED_GROUPS) - if (newList == null && oldList == null) { - return true - } else if (newList != null && oldList != null) { - if (newList.size != oldList.size) { - return false - } else { - for (item in newList) { - if (!oldList.contains(item.id)) { - return false - } + val oldList = userAccount.selectedGroupsList + if (newList.isEmpty() && oldList.isEmpty()) { + return false + } + if (newList.size != oldList.size) { + return false + } else { + for (item in newList) { + if (!oldList.contains(item.id)) { + return false } - return true } + return true + } + } + + override fun onChangeListener() { + if (compareSelectGroups()) { + binding.nextButton.isClickable = false + binding.nextButton.alpha = 0.8F } else { - return false + binding.nextButton.isClickable = true + binding.nextButton.alpha = 1F } } } diff --git a/app/src/main/java/org/xwiki/android/sync/activities/UserListAdapter.kt b/app/src/main/java/org/xwiki/android/sync/activities/UserListAdapter.kt index 7436c61e..3884a29d 100644 --- a/app/src/main/java/org/xwiki/android/sync/activities/UserListAdapter.kt +++ b/app/src/main/java/org/xwiki/android/sync/activities/UserListAdapter.kt @@ -19,16 +19,17 @@ */ package org.xwiki.android.sync.activities -import android.content.Context +import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.BaseAdapter import android.widget.CheckBox import android.widget.TextView +import androidx.annotation.Nullable +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView import org.xwiki.android.sync.R import org.xwiki.android.sync.bean.ObjectSummary -import org.xwiki.android.sync.bean.SearchResult /** * [android.widget.Adapter] which can be used to show [SearchResult] as users. @@ -43,59 +44,37 @@ import org.xwiki.android.sync.bean.SearchResult * @param searchResults Initial list */ -class UserListAdapter(private val mContext: Context, private var searchResults: List) - : BaseAdapter() { +class UserListAdapter(private var searchResults: List) + : RecyclerView.Adapter() { - /** - * @return Size of [.searchResults] - */ - override fun getCount(): Int { - return searchResults.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val itemView = LayoutInflater.from(parent.context).inflate(R.layout.list_item_group, parent, false) + return ViewHolder(itemView) } - /** - * @param position Position of item - * @return Item fron [.searchResults] by position - */ - override fun getItem(position: Int): ObjectSummary { - return searchResults[position] + override fun getItemCount(): Int { + return searchResults.size } - /** - * @param position Position of item - * @return position - */ - override fun getItemId(position: Int): Long { - return position.toLong() + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + viewHolder.groupNameTextView.text = searchResults.get(position).pageName + viewHolder.versionTextView.text = searchResults.get(position).wiki + viewHolder.checkBox.visibility = View.INVISIBLE } - /** - * Prepare and return view. - * - * @param position Position of item - * @param convertView Previous [View] - * @param parent Parent where view will be placed - * @return Result view - */ - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - var convertView = convertView - val viewHolder: ViewHolder - if (convertView == null) { - val inflater = LayoutInflater.from(mContext) - convertView = inflater.inflate(R.layout.list_item_group, null) - viewHolder = ViewHolder(convertView!!) - convertView.tag = viewHolder + override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList) { + if (payloads.isEmpty()) { + super.onBindViewHolder(holder, position, payloads) } else { - viewHolder = convertView.tag as ViewHolder + val o = payloads[0] as Bundle + for (key in o.keySet()) { + if (key == "guid") { + holder.groupNameTextView.setText(searchResults.get(position).pageName) + holder.versionTextView.setText(searchResults.get(position).wiki) + } + } } - - val item = getItem(position) - viewHolder.groupNameTextView.text = item.pageName - viewHolder.versionTextView.text = item.wiki - viewHolder.checkBox.visibility = View.INVISIBLE - convertView.setOnClickListener(null) - - return convertView } /** @@ -104,27 +83,57 @@ class UserListAdapter(private val mContext: Context, private var searchResults: * @param results New list */ fun refresh(results: List) { - if (searchResults != null && searchResults != results) { - searchResults = results - } - notifyDataSetChanged() + val diffResult = DiffUtil.calculateDiff(UserListDiffUtilCallBack(results, searchResults)) + diffResult.dispatchUpdatesTo(this) + searchResults = listOf() + this.searchResults = results } /** * Help ViewHolder. */ - private class ViewHolder(view: View) { + class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { val groupNameTextView: TextView val lastModifiedTime: TextView val versionTextView: TextView val checkBox: CheckBox init { - groupNameTextView = view.findViewById(R.id.groupName) - lastModifiedTime = view.findViewById(R.id.lastModifiedTime) - versionTextView = view.findViewById(R.id.version) - checkBox = view.findViewById(R.id.checkbox) + groupNameTextView = itemView.findViewById(R.id.groupName) + lastModifiedTime = itemView.findViewById(R.id.lastModifiedTime) + versionTextView = itemView.findViewById(R.id.version) + checkBox = itemView.findViewById(R.id.checkbox) } } + class UserListDiffUtilCallBack(internal var newList: List, internal var oldList: List) : + DiffUtil.Callback() { + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return newList[newItemPosition].id.equals(oldList[oldItemPosition].id) + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return newList[newItemPosition].id.equals(oldList[oldItemPosition].id) + } + + @Nullable + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { + val newModel = newList[newItemPosition] + val oldModel = oldList[oldItemPosition] + + val diff = Bundle() + + if (newModel.guid !== oldModel.guid) { + diff.putString("guid", newModel.guid) + } + return if (diff.size() == 0) { + null + } else diff + } + } } diff --git a/app/src/main/java/org/xwiki/android/sync/auth/AuthenticatorActivity.kt b/app/src/main/java/org/xwiki/android/sync/auth/AuthenticatorActivity.kt index 5bf1e699..7a0cad29 100644 --- a/app/src/main/java/org/xwiki/android/sync/auth/AuthenticatorActivity.kt +++ b/app/src/main/java/org/xwiki/android/sync/auth/AuthenticatorActivity.kt @@ -34,19 +34,23 @@ import android.util.Log import android.view.View import android.view.animation.AnimationUtils import android.view.inputmethod.InputMethodManager -import android.widget.Toast import androidx.appcompat.widget.Toolbar import androidx.databinding.DataBindingUtil +import kotlinx.coroutines.launch import org.xwiki.android.sync.* import org.xwiki.android.sync.activities.BaseViewFlipper import org.xwiki.android.sync.activities.SettingServerIpViewFlipper import org.xwiki.android.sync.activities.SignInViewFlipper import org.xwiki.android.sync.activities.SyncSettingsActivity +import org.xwiki.android.sync.contactdb.UserAccount import org.xwiki.android.sync.databinding.ActAuthenticatorBinding -import org.xwiki.android.sync.utils.* +import org.xwiki.android.sync.utils.PermissionsUtils +import org.xwiki.android.sync.utils.decrement +import org.xwiki.android.sync.utils.openLink +import org.xwiki.android.sync.utils.removeKeyValue import rx.Subscription import java.lang.reflect.InvocationTargetException -import java.util.ArrayList +import java.util.* /** * Tag which will be used for logging @@ -103,8 +107,9 @@ class AuthenticatorActivity : AccountAuthenticatorActivity() { */ private var mProgressDialog: Dialog? = null - var isTestRunning : Boolean = false + var isTestRunning: Boolean = false + var serverUrl: String? = null /** * Contains order of flippers in authorisation progress. @@ -168,13 +173,6 @@ class AuthenticatorActivity : AccountAuthenticatorActivity() { isTestRunning = intent.getBooleanExtra("Test", false) - if (!isTestRunning) { - if (availableAccounts.size > 0) { - Toast.makeText(this, "The user already exists!", Toast.LENGTH_SHORT).show() - finish() - return - } - } showViewFlipper(position) } @@ -258,7 +256,7 @@ class AuthenticatorActivity : AccountAuthenticatorActivity() { * @param view View which trigger action */ fun signUp(view: View) { - var url = currentBaseUrl() + var url = XWIKI_DEFAULT_SERVER_ADDRESS if (url.endsWith("/")) { url += "bin/view/XWiki/Registration" } else { @@ -355,12 +353,13 @@ class AuthenticatorActivity : AccountAuthenticatorActivity() { Log.d(TAG, "> finishLogin") //before add new account, clear old account data. - clearOldAccount() +// clearOldAccount() //get values val accountName = intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) val accountPassword = intent.getStringExtra(PARAM_USER_PASS) - val accountServer = intent.getStringExtra(PARAM_USER_SERVER) + val accountServer = serverUrl + val cookie = intent.getStringExtra(AccountManager.KEY_AUTHTOKEN) // Creating the account on the device and setting the auth token we got // (Not setting the auth token will cause another call to the server to authenticate the user) @@ -371,6 +370,15 @@ class AuthenticatorActivity : AccountAuthenticatorActivity() { mAccountManager.setUserData(account, AccountManager.KEY_PASSWORD, accountPassword) mAccountManager.setUserData(account, PARAM_USER_SERVER, accountServer) + appCoroutineScope.launch { + userAccountsRepo.createAccount( + UserAccount( + accountName, + accountServer.toString() + ) + ) + } + //grant permission if adding user from the third-party app (UID,PackageName); val packaName = getIntent().getStringExtra(PARAM_APP_PACKAGENAME) val uid = getIntent().getIntExtra(PARAM_APP_UID, 0) @@ -392,10 +400,13 @@ class AuthenticatorActivity : AccountAuthenticatorActivity() { setAccountAuthenticatorResult(intentReturn.extras) setResult(Activity.RESULT_OK, intentReturn) Log.d(TAG, ">" + "finish return") - finish() + val syncActivityIntent = Intent(this, SyncSettingsActivity::class.java) + syncActivityIntent.putExtra(AccountManager.KEY_ACCOUNT_NAME, accountName) + syncActivityIntent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE) startActivity( - Intent(this, SyncSettingsActivity::class.java) + syncActivityIntent ) + finish() } /** @@ -407,7 +418,7 @@ class AuthenticatorActivity : AccountAuthenticatorActivity() { * * @since 0.4.2 */ - fun showProgress(message: CharSequence, subscription: Subscription) { + fun showProgress(message: CharSequence, cancelCallback: () -> Unit) { // To avoid repeatedly create if (mProgressDialog != null && mProgressDialog!!.isShowing) { return @@ -418,7 +429,7 @@ class AuthenticatorActivity : AccountAuthenticatorActivity() { dialog.setCancelable(true) dialog.setOnCancelListener { Log.i(TAG, "user cancelling authentication") - subscription.unsubscribe() + cancelCallback() } // We save off the progress dialog in a field so that we can dismiss // it later. diff --git a/app/src/main/java/org/xwiki/android/sync/auth/XWikiAuthenticator.java b/app/src/main/java/org/xwiki/android/sync/auth/XWikiAuthenticator.java index 9c549423..825a30c4 100644 --- a/app/src/main/java/org/xwiki/android/sync/auth/XWikiAuthenticator.java +++ b/app/src/main/java/org/xwiki/android/sync/auth/XWikiAuthenticator.java @@ -38,6 +38,8 @@ import static org.xwiki.android.sync.AppContextKt.*; import static org.xwiki.android.sync.ConstantsKt.*; import static org.xwiki.android.sync.auth.AuthenticatorActivityKt.*; +import static org.xwiki.android.sync.utils.JavaCoroutinesBindingsKt.getUserAccountByAccountName; +import static org.xwiki.android.sync.utils.JavaCoroutinesBindingsKt.getUserServer; import static org.xwiki.android.sync.utils.SharedPrefsUtilsKt.getArrayList; /** @@ -162,7 +164,9 @@ public Bundle getAuthToken( authToken[0] = null; Log.d("xwiki", TAG + "> re-authenticating with the existing password"); final Object sync = new Object(); - getApiManager().getXwikiServicesApi().login( + resolveApiManager( + getUserAccountByAccountName(accountName) + ).getXwikiServicesApi().login( Credentials.basic(accountName, accountPassword) ).subscribe( new Action1>() { @@ -203,7 +207,7 @@ public void call(Response responseBodyResponse) { result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); result.putString(AccountManager.KEY_AUTHTOKEN, authToken[0]); - result.putString(SERVER_ADDRESS, currentBaseUrl()); + result.putString(SERVER_ADDRESS, getUserServer(account.name)); return result; } } @@ -277,8 +281,7 @@ public Bundle updateCredentials( } /** - * Refresh auth tokens for all packages which can be got by field - * {@link PACKAGE_LIST}. + * Refresh auth tokens for all packages which can be got by field PACKAGE_LIST. */ public static void refreshAllAuthTokenType( AccountManager am, diff --git a/app/src/main/java/org/xwiki/android/sync/bean/MutableInternalXWikiUserInfo.kt b/app/src/main/java/org/xwiki/android/sync/bean/MutableInternalXWikiUserInfo.kt index 4c5fbf4b..642f275c 100644 --- a/app/src/main/java/org/xwiki/android/sync/bean/MutableInternalXWikiUserInfo.kt +++ b/app/src/main/java/org/xwiki/android/sync/bean/MutableInternalXWikiUserInfo.kt @@ -1,7 +1,7 @@ package org.xwiki.android.sync.bean /** - * User info for use in-app. This bean will not be used between server and client - + * UserAccount info for use in-app. This bean will not be used between server and client - * only for internal usage. * * @version $Id$ diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/AccountAllUsersEntity.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/AccountAllUsersEntity.kt new file mode 100644 index 00000000..395d8449 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/AccountAllUsersEntity.kt @@ -0,0 +1,16 @@ +package org.xwiki.android.sync.contactdb + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.xwiki.android.sync.bean.ObjectSummary + +const val ALL_USERS_LIST_TABLE = "all_users_list_table" +const val AllUsersListColumn = "dataList" + +@Entity(tableName = ALL_USERS_LIST_TABLE) +data class AccountAllUsersEntity( + @PrimaryKey + @ColumnInfo(name = UserAccountIdColumn) val id: UserAccountId, + @ColumnInfo(name = AllUsersListColumn) var allUsersList: List +) \ No newline at end of file diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/AppDatabase.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/AppDatabase.kt new file mode 100644 index 00000000..62a07fd7 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/AppDatabase.kt @@ -0,0 +1,44 @@ +package org.xwiki.android.sync.contactdb + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import org.xwiki.android.sync.contactdb.dao.AccountsDao +import org.xwiki.android.sync.contactdb.dao.AllUsersCacheDao +import org.xwiki.android.sync.contactdb.dao.GroupsCacheDao +import org.xwiki.android.sync.utils.SelectedGroupsListConverter + + +@Database(entities = [UserAccount::class, AccountAllUsersEntity::class, GroupsCacheEntity::class], version = 1, exportSchema = false) +@TypeConverters(SelectedGroupsListConverter::class) +abstract class AppDatabase : RoomDatabase() { + abstract fun usersDao(): AccountsDao + abstract fun allUsersCacheDao(): AllUsersCacheDao + abstract fun groupsCacheDao(): GroupsCacheDao + + companion object { + + @Volatile + private var INSTANCE: AppDatabase? = null + + fun getInstance(context: Context): AppDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + "user.db" + ) + .fallbackToDestructiveMigration() + .build() + INSTANCE = instance + instance + } + } + + fun destroyInstance() { + INSTANCE = null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/ContactManager.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/ContactManager.kt index 5846c195..036bbef3 100644 --- a/app/src/main/java/org/xwiki/android/sync/contactdb/ContactManager.kt +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/ContactManager.kt @@ -19,116 +19,114 @@ */ package org.xwiki.android.sync.contactdb -import android.content.ContentProviderOperation import android.content.ContentResolver import android.content.ContentUris import android.content.Context -import android.content.res.AssetFileDescriptor -import android.database.Cursor import android.net.Uri -import android.os.Bundle -import android.os.CancellationSignal import android.provider.ContactsContract import android.provider.ContactsContract.RawContacts import android.util.Log import org.xwiki.android.sync.ACCOUNT_TYPE -import org.xwiki.android.sync.AppContext -import org.xwiki.android.sync.apiManager import org.xwiki.android.sync.bean.XWikiUserFull +import org.xwiki.android.sync.resolveApiManager +import org.xwiki.android.sync.rest.BaseApiManager import org.xwiki.android.sync.rest.XWikiHttp +import org.xwiki.android.sync.userAccountsRepo import retrofit2.HttpException import rx.Observable import rx.Observer import rx.functions.Action1 import rx.schedulers.Schedulers - import java.io.IOException -import java.io.OutputStream -import java.util.HashMap +import java.util.* /** * Class for managing contacts sync related mOperations. * * @version $Id: be8481217cc0fc4ab1ff4ab9e6f1b9d1fb4f5fc3 $ */ -object ContactManager { +class ContactManager( + private val apiManager: BaseApiManager +) { private val TAG = "ContactManager" - /** - * Subscribe to observable to get [XWikiUserFull] objects and save locally. - * - * @param context The context of Authenticator Activity - * @param account The username for the account - * @param observable Will be used to subscribe to get stream of users - * - * @since 0.4 - */ - @Synchronized @JvmStatic - fun updateContacts( - context: Context, - account: String, - observable: Observable - ) { - val resolver = context.contentResolver - val batchOperation = BatchOperation(resolver) - val localUserMaps = getAllContactsIdMap(resolver, account) + companion object { - observable.subscribeOn( - Schedulers.newThread() - ).subscribe( - object : Observer { - override fun onCompleted() { - for (id in localUserMaps.keys) { - val rawId = localUserMaps[id] - if (batchOperation.size() >= 100) { - batchOperation.execute() - } - } - batchOperation.execute() - } - override fun onError(e: Throwable) { - try { - val asHttpException = e as HttpException - if (asHttpException.code() == 401) {//Unauthorized - XWikiHttp.relogin( - context, - account - ) + /** + * Subscribe to observable to get [XWikiUserFull] objects and save locally. + * + * @param context The context of Authenticator Activity + * @param account The username for the account + * @param observable Will be used to subscribe to get stream of users + * + * @since 0.4 + */ + fun updateContacts( + context: Context, + account: UserAccount, + observable: Observable + ) { + val apiManager = resolveApiManager(account) + val contactManager = ContactManager(apiManager) + + val resolver = context.contentResolver + val batchOperation = BatchOperation(resolver) + val localUserMaps = contactManager.getAllContactsIdMap(resolver, account.accountName) + + observable.subscribe( + object : Observer { + override fun onCompleted() { + for (id in localUserMaps.keys) { + val rawId = localUserMaps[id] + if (batchOperation.size() >= 100) { + batchOperation.execute() + } } - } catch (e1: ClassCastException) { - Log.e(TAG, "Can't synchronize users", e) + batchOperation.execute() } - } + override fun onError(e: Throwable) { + try { + val asHttpException = e as HttpException + if (asHttpException.code() == 401) {//Unauthorized + apiManager.xWikiHttp.relogin( + context + ) + } + } catch (e1: ClassCastException) { + Log.e(contactManager.TAG, "Can't synchronize users", e) + } - override fun onNext(xWikiUserFull: XWikiUserFull) { - val operationList = xWikiUserFull.toContentProviderOperations( - resolver, - account - ) - for (operation in operationList) { - batchOperation.add(operation) } - if (batchOperation.size() >= 100) { - batchOperation.execute() + + override fun onNext(xWikiUserFull: XWikiUserFull) { + val operationList = xWikiUserFull.toContentProviderOperations( + resolver, + account.accountName + ) + for (operation in operationList) { + batchOperation.add(operation) + } + if (batchOperation.size() >= 100) { + batchOperation.execute() + } + contactManager.updateAvatar( + resolver, + contactManager.lookupRawContact(resolver, xWikiUserFull.id), + xWikiUserFull + ) } - updateAvatar( - resolver, - lookupRawContact(resolver, xWikiUserFull.id), - xWikiUserFull - ) } - } - ) + ) + } } - /** * Initiate procedure of contact avatar updating * * @param contentResolver Resolver to get contact photo file - * @param rawId User row id in local store + * @param rawId UserAccount row id in local store * @param xwikiUser Xwiki user info to find avatar * * @see .writeDisplayPhoto diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/ContactOperations.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/ContactOperations.kt index 388e3e06..466effea 100644 --- a/app/src/main/java/org/xwiki/android/sync/contactdb/ContactOperations.kt +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/ContactOperations.kt @@ -8,7 +8,9 @@ import android.content.ContentValues import android.database.Cursor import android.net.Uri import android.provider.ContactsContract -import org.xwiki.android.sync.* +import org.xwiki.android.sync.ACCOUNT_TYPE +import org.xwiki.android.sync.R +import org.xwiki.android.sync.appContext import org.xwiki.android.sync.bean.MutableInternalXWikiUserInfo import org.xwiki.android.sync.bean.XWikiUserFull import org.xwiki.android.sync.utils.extensions.getString @@ -98,9 +100,9 @@ fun XWikiUserFull.rowId( accountName: String ): Long { resolver.query( - ContactsContract.RawContacts.CONTENT_URI, + ContactsContract.Data.CONTENT_URI, arrayOf(ContactsContract.Data._ID), - "${ContactsContract.RawContacts.ACCOUNT_TYPE}=\"${ACCOUNT_TYPE}\" AND " + + "${ContactsContract.RawContacts.ACCOUNT_NAME}=\"${accountName}\" AND " + "${ContactsContract.RawContacts.SOURCE_ID}=\"$id\"", null, null diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/GroupsCacheEntity.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/GroupsCacheEntity.kt new file mode 100644 index 00000000..6dcb62ce --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/GroupsCacheEntity.kt @@ -0,0 +1,16 @@ +package org.xwiki.android.sync.contactdb + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.xwiki.android.sync.bean.XWikiGroup + +const val GROUPS_LIST_TABLE = "groups_list_table" +const val GroupsListColumn = "groupsList" + +@Entity(tableName = GROUPS_LIST_TABLE) +data class GroupsCacheEntity( + @PrimaryKey + @ColumnInfo(name = UserAccountIdColumn) val id: UserAccountId, + @ColumnInfo(name = GroupsListColumn) var groupsList: List +) \ No newline at end of file diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/UserAccount.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/UserAccount.kt new file mode 100644 index 00000000..a5ba541e --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/UserAccount.kt @@ -0,0 +1,25 @@ +package org.xwiki.android.sync.contactdb + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +typealias UserAccountId = Long + +const val USER_TABLE = "user_table" + +const val UserAccountIdColumn = "id" +const val UserAccountAccountNameColumn = "account_name" +const val UserAccountServerAddressColumn = "server_address" +const val UserAccountSelectedGroupsColumn = "selected_groups" +const val UserAccountSyncTypeColumn = "sync_type" + +@Entity (tableName = USER_TABLE) +data class UserAccount( + @ColumnInfo(name = UserAccountAccountNameColumn) val accountName: String, + @ColumnInfo(name = UserAccountServerAddressColumn) val serverAddress: String, + @ColumnInfo(name = UserAccountSelectedGroupsColumn) var selectedGroupsList: MutableList = mutableListOf(), + @ColumnInfo(name = UserAccountSyncTypeColumn) var syncType: Int = -1, + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = UserAccountIdColumn) val id: UserAccountId = 0 +) \ No newline at end of file diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/UserAccountCookies.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/UserAccountCookies.kt new file mode 100644 index 00000000..65b8c004 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/UserAccountCookies.kt @@ -0,0 +1,14 @@ +package org.xwiki.android.sync.contactdb + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +const val UserAccountCookieColumn = "cookie" + +@Entity(tableName = USER_TABLE) +data class UserAccountCookies( + @PrimaryKey + @ColumnInfo(name = UserAccountIdColumn) val id: UserAccountId, + @ColumnInfo(name = UserAccountCookieColumn) var cookie : String +) diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/abstracts/AllUsersCacheRepository.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/abstracts/AllUsersCacheRepository.kt new file mode 100644 index 00000000..b748a099 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/abstracts/AllUsersCacheRepository.kt @@ -0,0 +1,9 @@ +package org.xwiki.android.sync.contactdb.abstracts + +import org.xwiki.android.sync.bean.ObjectSummary +import org.xwiki.android.sync.contactdb.UserAccountId + +interface AllUsersCacheRepository { + operator fun get(id: UserAccountId): List? + operator fun set(id: UserAccountId, objects: List?) +} diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/abstracts/GroupsCacheRepository.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/abstracts/GroupsCacheRepository.kt new file mode 100644 index 00000000..68a92df2 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/abstracts/GroupsCacheRepository.kt @@ -0,0 +1,9 @@ +package org.xwiki.android.sync.contactdb.abstracts + +import org.xwiki.android.sync.bean.XWikiGroup +import org.xwiki.android.sync.contactdb.UserAccountId + +interface GroupsCacheRepository { + operator fun get(id: UserAccountId): List? + operator fun set(id: UserAccountId, groups: List?) +} diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/abstracts/UserAccountsCookiesRepository.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/abstracts/UserAccountsCookiesRepository.kt new file mode 100644 index 00000000..fda78787 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/abstracts/UserAccountsCookiesRepository.kt @@ -0,0 +1,8 @@ +package org.xwiki.android.sync.contactdb.abstracts + +import org.xwiki.android.sync.contactdb.UserAccountId + +interface UserAccountsCookiesRepository { + operator fun get(id: UserAccountId): String? + operator fun set(id: UserAccountId, cookies: String?) +} diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/abstracts/UserAccountsRepository.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/abstracts/UserAccountsRepository.kt new file mode 100644 index 00000000..14a2d724 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/abstracts/UserAccountsRepository.kt @@ -0,0 +1,19 @@ +package org.xwiki.android.sync.contactdb.abstracts + +import org.xwiki.android.sync.contactdb.UserAccount +import org.xwiki.android.sync.contactdb.UserAccountId + +interface UserAccountsRepository { + suspend fun createAccount(userAccount: UserAccount): UserAccount? + + suspend fun findByAccountName(name: String): UserAccount? + suspend fun findByAccountId(id: UserAccountId): UserAccount? + + suspend fun updateAccount(userAccount: UserAccount) + + suspend fun deleteAccount(id: UserAccountId) + + suspend fun getAll(): List +} + +suspend fun UserAccountsRepository.deleteAccount(userAccount: UserAccount) = deleteAccount(userAccount.id) diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/dao/AccountsDao.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/dao/AccountsDao.kt new file mode 100644 index 00000000..ae75a2c3 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/dao/AccountsDao.kt @@ -0,0 +1,28 @@ +package org.xwiki.android.sync.contactdb.dao + +import androidx.room.* +import org.xwiki.android.sync.contactdb.* + +@Dao +interface AccountsDao { + @Query ("SELECT * from USER_TABLE") + fun getAllAccount() : List + + @Query ("SELECT * FROM USER_TABLE WHERE $UserAccountAccountNameColumn LIKE :name") + fun findByAccountName(name: String): UserAccount? + + @Query ("SELECT * FROM USER_TABLE WHERE $UserAccountIdColumn=:id") + fun findById(id: UserAccountId): UserAccount? + + @Insert (onConflict = OnConflictStrategy.REPLACE) + fun insertAccount(userAccount: UserAccount): UserAccountId + + @Update (onConflict = OnConflictStrategy.REPLACE) + suspend fun updateUser(userAccount: UserAccount): Int + + @Query ("DELETE FROM USER_TABLE WHERE $UserAccountIdColumn = :userAccountId") + fun deleteUser(userAccountId: UserAccountId) + + @Query ("SELECT $UserAccountIdColumn FROM USER_TABLE WHERE $UserAccountServerAddressColumn = :serverUrl") + fun oneServerAccounts(serverUrl: String): List +} \ No newline at end of file diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/dao/AllUsersCacheDao.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/dao/AllUsersCacheDao.kt new file mode 100644 index 00000000..3586d40a --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/dao/AllUsersCacheDao.kt @@ -0,0 +1,24 @@ +package org.xwiki.android.sync.contactdb.dao + +import androidx.room.* +import org.xwiki.android.sync.bean.ObjectSummary +import org.xwiki.android.sync.contactdb.ALL_USERS_LIST_TABLE +import org.xwiki.android.sync.contactdb.AccountAllUsersEntity +import org.xwiki.android.sync.contactdb.UserAccountId +import org.xwiki.android.sync.contactdb.UserAccountIdColumn + +@Dao +interface AllUsersCacheDao { + @Query("SELECT * from $ALL_USERS_LIST_TABLE WHERE $UserAccountIdColumn LIKE :id") + operator fun get(id: UserAccountId): AccountAllUsersEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun set(syncTypeAccountAllUsersTable: AccountAllUsersEntity) + + @Query ("DELETE FROM $ALL_USERS_LIST_TABLE WHERE $UserAccountIdColumn = :id") + fun remove(id: UserAccountId) +} + +operator fun AllUsersCacheDao.set(id: UserAccountId, objects: List?) = objects ?.let { + set(AccountAllUsersEntity(id, objects)) +} ?: remove(id) \ No newline at end of file diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/dao/GroupsCacheDao.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/dao/GroupsCacheDao.kt new file mode 100644 index 00000000..f272e3f0 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/dao/GroupsCacheDao.kt @@ -0,0 +1,24 @@ +package org.xwiki.android.sync.contactdb.dao + +import androidx.room.* +import org.xwiki.android.sync.bean.XWikiGroup +import org.xwiki.android.sync.contactdb.GROUPS_LIST_TABLE +import org.xwiki.android.sync.contactdb.GroupsCacheEntity +import org.xwiki.android.sync.contactdb.UserAccountId +import org.xwiki.android.sync.contactdb.UserAccountIdColumn + +@Dao +interface GroupsCacheDao { + @Query("SELECT * from $GROUPS_LIST_TABLE where $UserAccountIdColumn LIKE :id") + operator fun get(id: UserAccountId) : GroupsCacheEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun set(groupsCacheEntity: GroupsCacheEntity) + + @Query ("DELETE FROM $GROUPS_LIST_TABLE WHERE $UserAccountIdColumn = :id") + fun remove(id: UserAccountId) +} + +operator fun GroupsCacheDao.set(id: UserAccountId, groups: List?) = groups ?.let { + set(GroupsCacheEntity(id, groups)) +} ?: remove(id) diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/dao_repositories/DAOAllUsersCacheRepository.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/dao_repositories/DAOAllUsersCacheRepository.kt new file mode 100644 index 00000000..f1bed2d3 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/dao_repositories/DAOAllUsersCacheRepository.kt @@ -0,0 +1,16 @@ +package org.xwiki.android.sync.contactdb.dao_repositories + +import org.xwiki.android.sync.bean.ObjectSummary +import org.xwiki.android.sync.contactdb.dao.AllUsersCacheDao +import org.xwiki.android.sync.contactdb.UserAccountId +import org.xwiki.android.sync.contactdb.abstracts.AllUsersCacheRepository +import org.xwiki.android.sync.contactdb.dao.set + +class DAOAllUsersCacheRepository( + private val allUsersCacheDao: AllUsersCacheDao +) : AllUsersCacheRepository { + override fun get(id: UserAccountId): List? = allUsersCacheDao[id] ?.allUsersList + override fun set(id: UserAccountId, objects: List?) { + allUsersCacheDao[id] = objects + } +} diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/dao_repositories/DAOGroupsCacheRepository.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/dao_repositories/DAOGroupsCacheRepository.kt new file mode 100644 index 00000000..7dce9cac --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/dao_repositories/DAOGroupsCacheRepository.kt @@ -0,0 +1,16 @@ +package org.xwiki.android.sync.contactdb.dao_repositories + +import org.xwiki.android.sync.bean.XWikiGroup +import org.xwiki.android.sync.contactdb.UserAccountId +import org.xwiki.android.sync.contactdb.abstracts.GroupsCacheRepository +import org.xwiki.android.sync.contactdb.dao.GroupsCacheDao +import org.xwiki.android.sync.contactdb.dao.set + +class DAOGroupsCacheRepository( + private val groupsCacheDao: GroupsCacheDao +) : GroupsCacheRepository { + override fun get(id: UserAccountId): List? = groupsCacheDao[id] ?.groupsList + override fun set(id: UserAccountId, groups: List?) { + groupsCacheDao[id] = groups + } +} diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/dao_repositories/DAOUserAccountsRepository.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/dao_repositories/DAOUserAccountsRepository.kt new file mode 100644 index 00000000..b3f8c54c --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/dao_repositories/DAOUserAccountsRepository.kt @@ -0,0 +1,44 @@ +package org.xwiki.android.sync.contactdb.dao_repositories + +import org.xwiki.android.sync.contactdb.UserAccount +import org.xwiki.android.sync.contactdb.UserAccountId +import org.xwiki.android.sync.contactdb.abstracts.AllUsersCacheRepository +import org.xwiki.android.sync.contactdb.abstracts.GroupsCacheRepository +import org.xwiki.android.sync.contactdb.abstracts.UserAccountsCookiesRepository +import org.xwiki.android.sync.contactdb.abstracts.UserAccountsRepository +import org.xwiki.android.sync.contactdb.dao.AccountsDao + +class DAOUserAccountsRepository ( + private val accountsDao: AccountsDao, + private val groupsCacheRepository: GroupsCacheRepository, + private val allUsersCacheRepository: AllUsersCacheRepository, + private val userAccountsCookiesRepository: UserAccountsCookiesRepository +) : UserAccountsRepository { + override suspend fun createAccount(userAccount: UserAccount): UserAccount? { + val id = accountsDao.insertAccount(userAccount) + return accountsDao.findById(id) + } + + override suspend fun findByAccountName(name: String): UserAccount? = accountsDao.findByAccountName(name) + override suspend fun findByAccountId(id: UserAccountId): UserAccount? = accountsDao.findById(id) + + override suspend fun updateAccount(userAccount: UserAccount) { + accountsDao.updateUser(userAccount) + } + + override suspend fun deleteAccount(id: UserAccountId) { + val user = findByAccountId(id) ?: return + + accountsDao.deleteUser(id) + + val otherServerUsers = accountsDao.oneServerAccounts(user.serverAddress) + + if (otherServerUsers.isEmpty()) { + groupsCacheRepository[id] = null + allUsersCacheRepository[id] = null + userAccountsCookiesRepository[id] = null + } + } + + override suspend fun getAll(): List = accountsDao.getAllAccount() +} \ No newline at end of file diff --git a/app/src/main/java/org/xwiki/android/sync/contactdb/shared_prefs_repositories/SharedPreferencesUserAccountsCookiesRepository.kt b/app/src/main/java/org/xwiki/android/sync/contactdb/shared_prefs_repositories/SharedPreferencesUserAccountsCookiesRepository.kt new file mode 100644 index 00000000..d017d1b8 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/contactdb/shared_prefs_repositories/SharedPreferencesUserAccountsCookiesRepository.kt @@ -0,0 +1,31 @@ +package org.xwiki.android.sync.contactdb.shared_prefs_repositories + +import android.content.Context +import android.content.SharedPreferences +import org.xwiki.android.sync.contactdb.UserAccountId +import org.xwiki.android.sync.contactdb.abstracts.UserAccountsCookiesRepository + +private const val cookiesSharedPreferences = "Cookies" + +class SharedPreferencesUserAccountsCookiesRepository( + private val context: Context +) : UserAccountsCookiesRepository { + private val sharedPreferences: SharedPreferences + get() = context.getSharedPreferences(cookiesSharedPreferences, Context.MODE_PRIVATE) + + override fun get(id: UserAccountId): String? { + return sharedPreferences.getString(id.toString(), null) + } + + override fun set(id: UserAccountId, cookies: String?) { + val sp = sharedPreferences + + val edit = sp.edit().apply { + cookies ?.let { + putString(id.toString(), it) + } ?: remove(id.toString()) + } + + edit.apply() + } +} diff --git a/app/src/main/java/org/xwiki/android/sync/rest/BaseApiManager.kt b/app/src/main/java/org/xwiki/android/sync/rest/BaseApiManager.kt index bf5cbcbb..bcb7840a 100644 --- a/app/src/main/java/org/xwiki/android/sync/rest/BaseApiManager.kt +++ b/app/src/main/java/org/xwiki/android/sync/rest/BaseApiManager.kt @@ -19,22 +19,37 @@ */ package org.xwiki.android.sync.rest -import android.content.Context import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor -import org.xwiki.android.sync.SERVER_ADDRESS -import org.xwiki.android.sync.utils.SharedPrefsUtils -import org.xwiki.android.sync.utils.getValue +import org.xwiki.android.sync.contactdb.UserAccountId +import org.xwiki.android.sync.contactdb.abstracts.UserAccountsCookiesRepository import retrofit2.Retrofit import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit /** * Will help to contain and separate functionality of working with API * + * Base constructor which create [OkHttpClient] with [XWikiInterceptor] which will + * be used in [Retrofit] for correct handling of requests. + * + * @param baseURL Url which will be used as base for all requests in this manager. Can be: + * + * * http://www.xwiki.org/xwiki/ + * * http://some.site.url/xwiki/ + * * http://123.231.213.132/xwiki/ + * * http://123.231.213.132:123456/xwiki/ + * + * Strongly recommended to end url with + * * @version $Id: 5f6167ab1821c765e6c4f42549b4447481e37cbc $ */ -class BaseApiManager { +class BaseApiManager( + baseURL: String, + userAccountId: UserAccountId, + userAccountsCookiesRepository: UserAccountsCookiesRepository +) { /** * Main services which provide work with auth, getting groups/users and other @@ -47,36 +62,25 @@ class BaseApiManager { /** * Helper which work with downloading and managing of photos * - * @since 0.4 - */ - /** * @return [.xWikiPhotosManager] * * @since 0.4 */ val xWikiPhotosManager: XWikiPhotosManager - /** - * Base constructor which create [OkHttpClient] with [XWikiInterceptor] which will - * be used in [Retrofit] for correct handling of requests. - * - * @param baseURL Url which will be used as base for all requests in this manager. Can be: - * - * * http://www.xwiki.org/xwiki/ - * * http://some.site.url/xwiki/ - * * http://123.231.213.132/xwiki/ - * * http://123.231.213.132:123456/xwiki/ - * - * Strongly recommended to end url with **/ + val xWikiHttp: XWikiHttp = XWikiHttp(this, userAccountId) - constructor(baseURL: String) { + init { var baseUrl = baseURL val loggingInterceptor = HttpLoggingInterceptor() loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY val okHttpClient = OkHttpClient.Builder() - .addInterceptor(XWikiInterceptor()) + .connectTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .addInterceptor(XWikiInterceptor(userAccountId, userAccountsCookiesRepository)) .addInterceptor(loggingInterceptor) .build() @@ -96,14 +100,6 @@ class BaseApiManager { xWikiPhotosManager = initXWikiPhotosManager(okHttpClient, baseUrl) } - /** - * Additional constructor which will try to get base url from [SharedPrefsUtils] using - * context and [Constants.SERVER_ADDRESS] preference name - * - * @param context Will be used to get info from shared preferences - */ - constructor(context: Context) : this(getValue(context, SERVER_ADDRESS, null)) {} - /** * Create [XWikiServices] using given [Retrofit] instance * diff --git a/app/src/main/java/org/xwiki/android/sync/rest/XWikiHttp.java b/app/src/main/java/org/xwiki/android/sync/rest/XWikiHttp.java index 846b73ee..dd77cd18 100644 --- a/app/src/main/java/org/xwiki/android/sync/rest/XWikiHttp.java +++ b/app/src/main/java/org/xwiki/android/sync/rest/XWikiHttp.java @@ -25,11 +25,13 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import kotlin.Pair; import okhttp3.Credentials; import okhttp3.ResponseBody; import org.xwiki.android.sync.bean.ObjectSummary; import org.xwiki.android.sync.bean.SerachResults.CustomObjectsSummariesContainer; import org.xwiki.android.sync.bean.XWikiUserFull; +import org.xwiki.android.sync.contactdb.UserAccount; import retrofit2.HttpException; import retrofit2.Response; import rx.Observable; @@ -37,16 +39,19 @@ import rx.functions.Action1; import rx.schedulers.Schedulers; import rx.subjects.PublishSubject; +import rx.subjects.Subject; + +import java.io.Closeable; import java.io.IOException; -import java.util.ArrayDeque; -import java.util.List; -import java.util.Map; -import java.util.Queue; +import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Semaphore; + import static org.xwiki.android.sync.AppContextKt.*; -import static org.xwiki.android.sync.ConstantsKt.*; -import static org.xwiki.android.sync.utils.SharedPrefsUtilsKt.*; +import static org.xwiki.android.sync.ConstantsKt.SYNC_TYPE_ALL_USERS; +import static org.xwiki.android.sync.ConstantsKt.SYNC_TYPE_SELECTED_GROUPS; +import static org.xwiki.android.sync.utils.JavaCoroutinesBindingsKt.getUserAccountName; +import static org.xwiki.android.sync.utils.JavaCoroutinesBindingsKt.getUserSyncType; /** * Static class which can be used as wrapper for a few requests such as login and other. It @@ -66,6 +71,16 @@ public class XWikiHttp { */ private static final String TAG = "XWikiHttp"; + private final BaseApiManager apiManager; + private final Long userAccountId; + private final String userAccountName; + + public XWikiHttp(BaseApiManager apiManager, Long userAccountId) { + this.apiManager = apiManager; + this.userAccountId = userAccountId; + this.userAccountName = getUserAccountName(userAccountId); + } + /** * Provide work with login. If be exactly - create login base credentials, send it via * {@link XWikiServices#login(String)}, extract cookies as auth token as a result and send it @@ -78,12 +93,12 @@ public class XWikiHttp { * * @since 0.4 */ - public static Observable login( + public Observable login( @NonNull String username, @NonNull String password ) { final PublishSubject authTokenSubject = PublishSubject.create(); - getApiManager().getXwikiServicesApi().login( + apiManager.getXwikiServicesApi().login( Credentials.basic(username, password) ).subscribeOn( Schedulers.newThread() @@ -93,11 +108,6 @@ public static Observable login( public void call(Response responseBodyResponse) { if (responseBodyResponse.code() >= 200 && responseBodyResponse.code() <= 209) { String cookie = responseBodyResponse.headers().get("Set-Cookie"); - putValue( - getAppContext().getApplicationContext(), - COOKIE, - cookie - ); authTokenSubject.onNext(cookie); } else { authTokenSubject.onNext(null); @@ -119,22 +129,20 @@ public void call(Throwable throwable) { * Relogin for account * * @param context Context to get {@link AccountManager} and other data - * @param accountName Name of account to know which user must be relogged in * @return Observable to know when already authorized * * @since 0.5 */ @Nullable - public static Observable relogin( - Context context, - String accountName + public Observable relogin( + Context context ) { AccountManager accountManager = AccountManager.get(context); Account account = null; for (Account current : accountManager.getAccounts()) { - if (current.name.equals(accountName)) { + if (current.name.equals(userAccountName)) { account = current; break; } @@ -144,7 +152,7 @@ public static Observable relogin( return null; } else { Observable loginObservable = login( - accountName, + account.name, accountManager.getPassword(account) ); loginObservable.subscribe( @@ -168,19 +176,17 @@ public void call(String s) { * be called on first error of getting users and receiving of users will be stopped. * {@link Observer#onCompleted()} will be called when receiving of user was successfully * completed - * - * @see SYNC_TYPE_ALL_USERS - * @see SYNC_TYPE_SELECTED_GROUPS */ - public static Observable getSyncData( + public Pair, Thread> getSyncData( final int syncType, - final String accountName + final List selectedGroups ) { final PublishSubject subject = PublishSubject.create(); + Thread newThread = null; final Semaphore semaphore = new Semaphore(1); try { semaphore.acquire(); - new Thread( + newThread = new Thread( new Runnable() { @Override public void run() { @@ -196,15 +202,12 @@ public void run() { } if (syncType == SYNC_TYPE_ALL_USERS) { getSyncAllUsers( - subject, - accountName + subject ); } else if (syncType == SYNC_TYPE_SELECTED_GROUPS) { - List groupIdList = getArrayList(getAppContext().getApplicationContext(), SELECTED_GROUPS); getSyncGroups( - groupIdList, - subject, - accountName + selectedGroups, + subject ); } else { throw new IOException(TAG + "syncType error, SyncType=" + syncType); @@ -214,12 +217,12 @@ public void run() { } } } - ).start(); + ); + newThread.start(); semaphore.acquire(); - } catch (InterruptedException e) { - return subject; + } finally { + return new Pair(subject, newThread); } - return subject; } /** @@ -228,17 +231,15 @@ public void run() { * * @param groupIdList Contains ids of groups to sync * @param subject Contains subject to send updates info - * @param account Account which used for sync * @throws IOException Will be thrown if something went wrong * - * @see #getDetailedInfo(List, PublishSubject, String) + * @see #getDetailedInfo(List, PublishSubject) * * @since 0.4 */ - private static void getSyncGroups( + private void getSyncGroups( @NonNull List groupIdList, - @NonNull final PublishSubject subject, - @NonNull final String account + @NonNull final PublishSubject subject ) throws IOException { final CountDownLatch groupsCountDown = new CountDownLatch(groupIdList.size()); @@ -252,7 +253,7 @@ private static void getSyncGroups( subject.onError(exception); throw exception; } - getApiManager().getXwikiServicesApi().getGroupMembers( + apiManager.getXwikiServicesApi().getGroupMembers( split[0], split[1], split[2] @@ -262,8 +263,7 @@ private static void getSyncGroups( public void call(CustomObjectsSummariesContainer summaries) { getDetailedInfo( summaries.getObjectSummaries(), - subject, - account + subject ); groupsCountDown.countDown(); } @@ -291,14 +291,12 @@ public void call(Throwable throwable) { * * @param from Objects which can be used to get info about users to load them * @param subject Contains subject to send updates info - * @param account Account which used for sync * * @since 0.4 */ - private static void getDetailedInfo( + private void getDetailedInfo( @NonNull List from, - @NonNull final PublishSubject subject, - @NonNull final String account + @NonNull final PublishSubject subject ) { final Queue queueOfSummaries = new ArrayDeque<>(from); @@ -311,7 +309,7 @@ private static void getDetailedInfo( if (spaceAndName == null) { continue; } - getApiManager().getXwikiServicesApi().getFullUserDetails( + apiManager.getXwikiServicesApi().getFullUserDetails( spaceAndName.getKey(), spaceAndName.getValue() ).subscribe( @@ -324,9 +322,8 @@ public void onError(Throwable e) { try { HttpException asHttpException = (HttpException) e; if (asHttpException.code() == 401) {//Unauthorized - XWikiHttp.relogin( - getAppContext(), - account + relogin( + getAppContext() ).subscribe( new Observer() { @Override @@ -370,7 +367,7 @@ public void onNext(XWikiUserFull userFull) { } /** - * Start to sync all users using {@link getAppContextInstance()}. In + * Start to sync all users. In * {@link Observer#onNext(Object)} you will receive each user which was correctly received. * {@link Observer#onError(Throwable)} will be called on first error of getting users and * receiving of users will be stopped. {@link Observer#onCompleted()} will be called when @@ -378,15 +375,14 @@ public void onNext(XWikiUserFull userFull) { * * @param subject Will be used as object for events */ - private static void getSyncAllUsers( - final PublishSubject subject, - final String account + private void getSyncAllUsers( + final PublishSubject subject ) { final Queue searchList = new ArrayDeque<>(); final Semaphore semaphore = new Semaphore(1); try { semaphore.acquire(); - getApiManager().getXwikiServicesApi().getAllUsersPreview().subscribe( + apiManager.getXwikiServicesApi().getAllUsersPreview().subscribe( new Action1>() { @Override public void call(CustomObjectsSummariesContainer summaries) { @@ -404,6 +400,7 @@ public void call(Throwable throwable) { ); } catch (InterruptedException e) { Log.e(TAG, "Can't await synchronize all users", e); + return; } try { @@ -415,6 +412,7 @@ public void call(Throwable throwable) { } catch (InterruptedException e) { e.printStackTrace(); Log.e(TAG, "Can't await synchronize all users", e); + return; } while (subject.getThrowable() == null && !searchList.isEmpty()) { @@ -423,7 +421,7 @@ public void call(Throwable throwable) { if (subject.getThrowable() != null) {// was was not error in sync return; } - getApiManager().getXwikiServicesApi().getFullUserDetails( + apiManager.getXwikiServicesApi().getFullUserDetails( item.getWiki(), item.getSpace(), item.getPageName() @@ -438,9 +436,8 @@ public void onError(Throwable e) { try { HttpException asHttpException = (HttpException) e; if (asHttpException.code() == 401) {//Unauthorized - XWikiHttp.relogin( - getAppContext(), - account + relogin( + getAppContext() ).subscribe( new Observer() { @Override @@ -481,7 +478,7 @@ public void onNext(XWikiUserFull userFull) { // if many users should be synchronized, the task will not be stop // even though you close the sync in settings or selecting the "don't sync" option. // we should stop the task by checking the sync type each time. - int syncType = getValue(getAppContext().getApplicationContext(), SYNC_TYPE, -1); + int syncType = getUserSyncType(userAccountId); if (syncType != SYNC_TYPE_ALL_USERS) { IOException exception = new IOException("the sync type has been changed"); subject.onError(exception); diff --git a/app/src/main/java/org/xwiki/android/sync/rest/XWikiInterceptor.kt b/app/src/main/java/org/xwiki/android/sync/rest/XWikiInterceptor.kt index 86e0b74a..aa43c1e8 100644 --- a/app/src/main/java/org/xwiki/android/sync/rest/XWikiInterceptor.kt +++ b/app/src/main/java/org/xwiki/android/sync/rest/XWikiInterceptor.kt @@ -20,12 +20,12 @@ package org.xwiki.android.sync.rest import android.text.TextUtils +import kotlinx.coroutines.launch import okhttp3.Interceptor import okhttp3.Response -import org.xwiki.android.sync.* -import org.xwiki.android.sync.utils.SharedPrefsUtils -import org.xwiki.android.sync.utils.getValue - +import org.xwiki.android.sync.appCoroutineScope +import org.xwiki.android.sync.contactdb.UserAccountId +import org.xwiki.android.sync.contactdb.abstracts.UserAccountsCookiesRepository import java.io.IOException private const val HEADER_CONTENT_TYPE = "Content-type" @@ -41,20 +41,10 @@ private const val CONTENT_TYPE = "application/json" * * @version $Id: 374209a130ca477ae567048f6f4a129ace2ea0d1 $ */ -class XWikiInterceptor : Interceptor { - - /** - * @return [SharedPrefsUtils.getValue] with key - * [Constants.COOKIE] and def value **empty string** - * - * @since 0.4 - */ - private val cookie: String - get() = getValue( - appContext.applicationContext, - COOKIE, - "" - ) +class XWikiInterceptor( + private val userAccountId: UserAccountId, + private val userAccountsCookiesRepository: UserAccountsCookiesRepository +) : Interceptor { /** * Add query parameter **media=json**, headers [.HEADER_ACCEPT]=[.CONTENT_TYPE] @@ -80,10 +70,14 @@ class XWikiInterceptor : Interceptor { .header(HEADER_ACCEPT, CONTENT_TYPE) .url(url) - val cookie = cookie + var cookie: String? = null + + appCoroutineScope.launch { + cookie = userAccountsCookiesRepository[userAccountId] + } if (!TextUtils.isEmpty(cookie)) { - builder.addHeader(HEADER_COOKIE, cookie) + builder.addHeader(HEADER_COOKIE, cookie.toString()) } val request = builder.build() diff --git a/app/src/main/java/org/xwiki/android/sync/syncadapter/SyncAdapter.java b/app/src/main/java/org/xwiki/android/sync/syncadapter/SyncAdapter.java deleted file mode 100644 index 4b3ef10c..00000000 --- a/app/src/main/java/org/xwiki/android/sync/syncadapter/SyncAdapter.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * See the NOTICE file distributed with this work for additional - * information regarding copyright ownership. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.xwiki.android.sync.syncadapter; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.AbstractThreadedSyncAdapter; -import android.content.ContentProviderClient; -import android.content.Context; -import android.content.SyncResult; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import org.xwiki.android.sync.bean.XWikiUserFull; -import org.xwiki.android.sync.contactdb.ContactManager; -import org.xwiki.android.sync.rest.XWikiHttp; -import org.xwiki.android.sync.utils.StringUtils; -import rx.Observable; -import rx.Observer; - -import java.util.Date; - -import static org.xwiki.android.sync.ConstantsKt.*; -import static org.xwiki.android.sync.contactdb.ContactOperationsKt.setAccountContactsVisibility; -import static org.xwiki.android.sync.utils.SharedPrefsUtilsKt.getValue; - -/** - * Adapter which will be used for synchronization. - * - * @version $Id$ - */ -public class SyncAdapter extends AbstractThreadedSyncAdapter { - - /** - * Tag for logging. - */ - private static final String TAG = "SyncAdapter"; - - /** - * Account manager to manage synchronization. - */ - private final AccountManager mAccountManager; - - /** - * Context for all operations. - */ - private final Context mContext; - - /** - * @param context will be set to {@link #mContext} - * @param autoInitialize auto initialization sync - * @param allowParallelSyncs flag about paralleling of sync - */ - public SyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) { - super(context, autoInitialize, allowParallelSyncs); - mContext = context; - mAccountManager = AccountManager.get(context); - } - - /** - * @param context will be set to {@link #mContext} - * @param autoInitialize auto initialization sync - */ - public SyncAdapter(Context context, boolean autoInitialize) { - super(context, autoInitialize); - mContext = context; - mAccountManager = AccountManager.get(context); - } - - /** - * Perform all sync process. - * - * @param account the account that should be synced - * @param extras SyncAdapter-specific parameters - * @param authority the authority of this sync request - * @param provider a ContentProviderClient that points to the ContentProvider for this - * authority - * @param syncResult SyncAdapter-specific parameters - */ - @Override - public void onPerformSync( - final Account account, - Bundle extras, - String authority, - ContentProviderClient provider, - final SyncResult syncResult) - { - setAccountContactsVisibility( - getContext().getContentResolver(), - account, - false - ); - Log.i(TAG, "onPerformSync start"); - int syncType = getValue( - mContext, - SYNC_TYPE, - SYNC_TYPE_NO_NEED_SYNC - ); - Log.i(TAG, "syncType=" + syncType); - if (syncType == SYNC_TYPE_NO_NEED_SYNC) return; - // get last sync date. return new Date(0) if first onPerformSync - String lastSyncMarker = getServerSyncMarker(account); - Log.d(TAG, lastSyncMarker); - - // Get XWiki SyncData from XWiki server , which should be added, updated or deleted after lastSyncMarker. - final Observable observable = XWikiHttp.getSyncData( - syncType, - account.name - ); - - // Update the local contacts database with the last modified changes. updateContact() - ContactManager.updateContacts(mContext, account.name, observable); - - final Object[] sync = new Object[]{null}; - - observable.subscribe( - new Observer() { - @Override - public void onCompleted() { - // Save off the new sync date. On our next sync, we only want to receive - // contacts that have changed since this sync... - setServerSyncMarker(account, StringUtils.dateToIso8601String(new Date())); - synchronized (sync) { - sync[0] = new Object(); - sync.notifyAll(); - } - setAccountContactsVisibility( - getContext().getContentResolver(), - account, - true - ); - } - - @Override - public void onError(Throwable e) { - syncResult.stats.numIoExceptions++; - synchronized (sync) { - sync[0] = new Object(); - sync.notifyAll(); - } - setAccountContactsVisibility( - getContext().getContentResolver(), - account, - true - ); - } - - @Override - public void onNext(XWikiUserFull xWikiUserFull) { - syncResult.stats.numEntries++; - } - } - ); - - synchronized (observable) { - observable.notifyAll(); - } - synchronized (sync) { - while (sync[0] == null) { - try { - sync.wait(); - } catch (InterruptedException e) { - Log.e(TAG, "Can't await end of sync", e); - } - } - } - } - - /** - * This helper function fetches the last known high-water-mark - * we received from the server - or 0 if we've never synced. - * - * @param account the account we're syncing - * @return the change high-water-mark Iso8601 - */ - private String getServerSyncMarker(Account account) { - String lastSyncIso = mAccountManager.getUserData(account, SYNC_MARKER_KEY); - //if empty, just return new Date(0) so that we can get all users from server. - if (TextUtils.isEmpty(lastSyncIso)) { - return StringUtils.dateToIso8601String(new Date(0)); - } - return lastSyncIso; - } - - /** - * Save off the high-water-mark we receive back from the server. - * - * @param account The account we're syncing - * @param lastSyncIso The high-water-mark we want to save. - */ - private void setServerSyncMarker(Account account, String lastSyncIso) { - mAccountManager.setUserData(account, SYNC_MARKER_KEY, lastSyncIso); - } -} - diff --git a/app/src/main/java/org/xwiki/android/sync/syncadapter/SyncAdapter.kt b/app/src/main/java/org/xwiki/android/sync/syncadapter/SyncAdapter.kt new file mode 100644 index 00000000..abdba01f --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/syncadapter/SyncAdapter.kt @@ -0,0 +1,207 @@ +package org.xwiki.android.sync.syncadapter + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.AbstractThreadedSyncAdapter +import android.content.ContentProviderClient +import android.content.Context +import android.content.SyncResult +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import org.xwiki.android.sync.* +import org.xwiki.android.sync.bean.XWikiUserFull +import org.xwiki.android.sync.contactdb.ContactManager +import org.xwiki.android.sync.contactdb.setAccountContactsVisibility +import org.xwiki.android.sync.rest.XWikiHttp +import org.xwiki.android.sync.utils.ChannelJavaWaiter +import org.xwiki.android.sync.utils.StringUtils +import org.xwiki.android.sync.utils.awaitBlocking +import rx.Observer +import java.util.* + +private const val TAG = "SyncAdapter" + +class SyncAdapter( + context: Context, + autoInitialize: Boolean, + allowParallelSyncs: Boolean +) : AbstractThreadedSyncAdapter( + context, + autoInitialize, + allowParallelSyncs +) { + /** + * Account manager to manage synchronization. + */ + private val mAccountManager: AccountManager = AccountManager.get(context) + + /** + * @param context will be set to {@link #mContext} + * @param autoInitialize auto initialization sync + */ + constructor( + context: Context, + autoInitialize: Boolean + ) : this( + context, + autoInitialize, + false + ) + + private var updateThread: Thread? = null + set(value) { + synchronized(this) { + field ?.also { + if (it.isAlive) { + it.interrupt() + } + } + field = value + } + } + + /** + * Perform all sync process. + * + * @param account the account that should be synced + * @param extras SyncAdapter-specific parameters + * @param authority the authority of this sync request + * @param provider a ContentProviderClient that points to the ContentProvider for this + * authority + * @param syncResult SyncAdapter-specific parameters + */ + override fun onPerformSync( + account: Account?, + extras: Bundle?, + authority: String?, + provider: ContentProviderClient?, + syncResult: SyncResult + ) { + account ?: return + setAccountContactsVisibility( + context.contentResolver, + account, + false + ) + + val triggeringChannel = Channel(1) + val channelJavaWaiter = ChannelJavaWaiter(appCoroutineScope, triggeringChannel) + + val updateJob = appCoroutineScope.async { + Log.i(TAG, "onPerformSync start") + val userAccount = userAccountsRepo.findByAccountName(account.name) ?: return@async null + val syncType = userAccount.syncType + + Log.i(TAG, "syncType=$syncType") + if (syncType == SYNC_TYPE_NO_NEED_SYNC) return@async null + // get last sync date. return new Date(0) if first onPerformSync + val lastSyncMarker = getServerSyncMarker(account) + Log.d(TAG, lastSyncMarker) + + // Get XWiki SyncData from XWiki server , which should be added, updated or deleted after lastSyncMarker. + val (observable, thread) = resolveApiManager(userAccount).xWikiHttp.getSyncData( + syncType, + userAccount.selectedGroupsList + ) + + thread.setUncaughtExceptionHandler { _, e -> + observable.onError(e) + } + + updateThread = thread + + // Update the local contacts database with the last modified changes. updateContact() + ContactManager.updateContacts(context, userAccount, observable) + + observable.subscribe( + object : Observer { + override fun onCompleted() { + updateServerSyncMarker(account) + + triggeringChannel.offer(true) + + setAccountContactsVisibility( + context.contentResolver, + account, + true + ) + } + + override fun onError(e: Throwable) { + syncResult.stats.numIoExceptions++ + + triggeringChannel.offer(true) + + setAccountContactsVisibility( + context.contentResolver, + account, + true + ) + } + + override fun onNext(xWikiUserFull: XWikiUserFull) { + syncResult.stats.numEntries++ + } + } + ).also { + synchronized(observable) { + (observable as Object).notifyAll() + } + } + } + + updateJob.invokeOnCompletion { error -> + if (error != null) { + triggeringChannel.offer(true) + } + } + + try { + channelJavaWaiter.lock() + } catch (e: InterruptedException) { + updateJob.awaitBlocking(appCoroutineScope) ?.unsubscribe() + } finally { + updateThread = null + } + } + + override fun onSyncCanceled() { + super.onSyncCanceled() + updateThread = null + } + + /** + * This helper function fetches the last known high-water-mark + * we received from the server - or 0 if we've never synced. + * + * @param account the account we're syncing + * @return the change high-water-mark Iso8601 + */ + private fun getServerSyncMarker(account: Account): String { + val lastSyncIso = mAccountManager.getUserData(account, SYNC_MARKER_KEY) + //if empty, just return new Date(0) so that we can get all users from server. + return if (TextUtils.isEmpty(lastSyncIso)) { + StringUtils.dateToIso8601String(Date(0)) + } else lastSyncIso + } + + /** + * Save off the high-water-mark we receive back from the server. + * + * @param account The account we're syncing + * @param lastSyncIso The high-water-mark we want to save. + */ + private fun setServerSyncMarker(account: Account, lastSyncIso: String) { + mAccountManager.setUserData(account, SYNC_MARKER_KEY, lastSyncIso) + } + + private fun updateServerSyncMarker(account: Account) { + // Save off the new sync date. On our next sync, we only want to receive + // contacts that have changed since this sync... + setServerSyncMarker(account, StringUtils.dateToIso8601String(Date())) + } +} diff --git a/app/src/main/java/org/xwiki/android/sync/utils/AccountClickListener.kt b/app/src/main/java/org/xwiki/android/sync/utils/AccountClickListener.kt new file mode 100644 index 00000000..b9326793 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/utils/AccountClickListener.kt @@ -0,0 +1,5 @@ +package org.xwiki.android.sync.utils + +import android.accounts.Account + +interface AccountClickListener : (Account) -> Unit \ No newline at end of file diff --git a/app/src/main/java/org/xwiki/android/sync/utils/AccountsUpdaterListener.kt b/app/src/main/java/org/xwiki/android/sync/utils/AccountsUpdaterListener.kt new file mode 100644 index 00000000..c8653794 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/utils/AccountsUpdaterListener.kt @@ -0,0 +1,37 @@ +package org.xwiki.android.sync.utils + +import android.accounts.Account +import android.accounts.AccountManager +import android.accounts.OnAccountsUpdateListener +import kotlinx.coroutines.launch +import org.xwiki.android.sync.ACCOUNT_TYPE +import org.xwiki.android.sync.appContext +import org.xwiki.android.sync.appCoroutineScope +import org.xwiki.android.sync.userAccountsRepo + +fun AccountManager.enableDetectingOfAccountsRemoving() { + addOnAccountsUpdatedListener( + AccountsUpdateListener(), + null, + true + ) +} + +internal class AccountsUpdateListener : OnAccountsUpdateListener { + override fun onAccountsUpdated(accounts: Array) { + val accountManager = AccountManager.get(appContext) + + val internalAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE).map { + it.name + } + + appCoroutineScope.launch { + val allUsers = userAccountsRepo.getAll() + allUsers.forEach { + if (it.accountName !in internalAccounts) { + userAccountsRepo.deleteAccount(it.id) + } + } + } + } +} diff --git a/app/src/main/java/org/xwiki/android/sync/utils/ChannelJavaWaiter.kt b/app/src/main/java/org/xwiki/android/sync/utils/ChannelJavaWaiter.kt new file mode 100644 index 00000000..e42632ce --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/utils/ChannelJavaWaiter.kt @@ -0,0 +1,61 @@ +package org.xwiki.android.sync.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.lang.Exception + +fun Deferred.awaitBlocking(scope: CoroutineScope): T? { + val channel = Channel(1) + var result: T? = null + + val waiter = ChannelJavaWaiter( + scope, + channel + ) + + scope.launch { + try { + val awaited = await() + result = awaited + } finally { + channel.send(true) + } + } + + waiter.lock() + return result +} + +class ChannelJavaWaiter( + scope: CoroutineScope, + private val channelToWait: ReceiveChannel +) { + private val lockObject = Object() + private var lockChecker: Boolean = false + + init { + scope.launch { + while (isActive && !lockChecker) { + if (channelToWait.receive()) { + synchronized(lockObject) { + lockChecker = true + lockObject.notifyAll() + } + } + } + } + } + + @Throws(InterruptedException::class) + fun lock() { + synchronized(lockObject) { + while (!lockChecker) { + lockObject.wait() + } + } + } +} diff --git a/app/src/main/java/org/xwiki/android/sync/utils/GroupsListCangeListener.kt b/app/src/main/java/org/xwiki/android/sync/utils/GroupsListCangeListener.kt new file mode 100644 index 00000000..a6736712 --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/utils/GroupsListCangeListener.kt @@ -0,0 +1,6 @@ +package org.xwiki.android.sync.utils + +interface GroupsListChangeListener{ + + fun onChangeListener() +} \ No newline at end of file diff --git a/app/src/main/java/org/xwiki/android/sync/utils/JavaCoroutinesBindings.kt b/app/src/main/java/org/xwiki/android/sync/utils/JavaCoroutinesBindings.kt new file mode 100644 index 00000000..9e03a37d --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/utils/JavaCoroutinesBindings.kt @@ -0,0 +1,39 @@ +package org.xwiki.android.sync.utils + +import kotlinx.coroutines.async +import org.xwiki.android.sync.SYNC_TYPE_NO_NEED_SYNC +import org.xwiki.android.sync.XWIKI_DEFAULT_SERVER_ADDRESS +import org.xwiki.android.sync.appCoroutineScope +import org.xwiki.android.sync.contactdb.UserAccount +import org.xwiki.android.sync.contactdb.UserAccountId +import org.xwiki.android.sync.userAccountsRepo + +fun getUserAccountById(accountId: UserAccountId): UserAccount? { + return appCoroutineScope.async { + userAccountsRepo.findByAccountId(accountId) + }.awaitBlocking( + appCoroutineScope + ) +} + +fun getUserAccountByAccountName(accountName: String): UserAccount? { + return appCoroutineScope.async { + userAccountsRepo.findByAccountName(accountName) + }.awaitBlocking( + appCoroutineScope + ) +} + +fun getUserSyncType(accountId: UserAccountId): Int { + return getUserAccountById(accountId) ?.syncType ?: SYNC_TYPE_NO_NEED_SYNC +} + +fun getUserAccountName(accountId: UserAccountId): String? { + return getUserAccountById(accountId) ?.accountName +} + +fun getUserServer(accountName: String?): String { + return accountName ?.let { + getUserAccountByAccountName(accountName) ?.serverAddress + } ?: XWIKI_DEFAULT_SERVER_ADDRESS +} diff --git a/app/src/main/java/org/xwiki/android/sync/utils/SelectedGroupsListConverter.kt b/app/src/main/java/org/xwiki/android/sync/utils/SelectedGroupsListConverter.kt new file mode 100644 index 00000000..0ed894ab --- /dev/null +++ b/app/src/main/java/org/xwiki/android/sync/utils/SelectedGroupsListConverter.kt @@ -0,0 +1,66 @@ +package org.xwiki.android.sync.utils + +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import org.xwiki.android.sync.bean.ObjectSummary +import org.xwiki.android.sync.bean.XWikiGroup +import java.util.* + + +class SelectedGroupsListConverter{ + private val gson = Gson() + @TypeConverter + fun stringToList(data: String?): List { + if (data == null) { + return Collections.emptyList() + } + + val listType = object : TypeToken>() { + + }.type + + return gson.fromJson>(data, listType) + } + + @TypeConverter + fun listToString(someObjects: List): String { + return gson.toJson(someObjects) + } + + @TypeConverter + fun stringToXWikiGroupList(data: String?): MutableList { + if (data == null) { + return Collections.emptyList() + } + + val listType = object : TypeToken>() { + + }.type + + return gson.fromJson>(data, listType) + } + + @TypeConverter + fun XWikiGroupListToString(someObjects: MutableList): String { + return gson.toJson(someObjects) + } + + @TypeConverter + fun stringToXWikiAllUserList(data: String?): MutableList { + if (data == null) { + return Collections.emptyList() + } + + val listType = object : TypeToken>() { + + }.type + + return gson.fromJson>(data, listType) + } + + @TypeConverter + fun XWikiAllUserListToString(someObjects: MutableList): String { + return gson.toJson(someObjects) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xwiki/android/sync/utils/StringUtils.kt b/app/src/main/java/org/xwiki/android/sync/utils/StringUtils.kt index 16c76118..e3fb0b31 100644 --- a/app/src/main/java/org/xwiki/android/sync/utils/StringUtils.kt +++ b/app/src/main/java/org/xwiki/android/sync/utils/StringUtils.kt @@ -19,14 +19,11 @@ */ package org.xwiki.android.sync.utils -import android.text.TextUtils -import org.xwiki.android.sync.currentBaseUrl import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; import java.util.regex.Pattern; -import java.util.regex.Matcher /** * String Utils diff --git a/app/src/main/res/drawable/ic_edit_black_24dp.xml b/app/src/main/res/drawable/ic_edit_black_24dp.xml new file mode 100644 index 00000000..2ab2fb75 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/account_list_layout.xml b/app/src/main/res/layout/account_list_layout.xml new file mode 100644 index 00000000..e08befb5 --- /dev/null +++ b/app/src/main/res/layout/account_list_layout.xml @@ -0,0 +1,29 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/act_select_account.xml b/app/src/main/res/layout/act_select_account.xml new file mode 100644 index 00000000..090ea7ec --- /dev/null +++ b/app/src/main/res/layout/act_select_account.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_sync_settings.xml b/app/src/main/res/layout/activity_sync_settings.xml index be349392..eea32c36 100644 --- a/app/src/main/res/layout/activity_sync_settings.xml +++ b/app/src/main/res/layout/activity_sync_settings.xml @@ -1,10 +1,10 @@ - + - + @@ -19,35 +19,123 @@ android:orientation="vertical" android:layout_alignParentTop="true" android:layout_above="@+id/nextButton"> - - + android:orientation="vertical"> + + + + android:layout_marginStart="8dp" + android:padding="@dimen/defaultViewSmallPadding" + android:layout_marginLeft="8dp"/>