diff --git a/app/res/layout/entity_select_layout.xml b/app/res/layout/entity_select_layout.xml index f59231a47f..307cb99d0f 100644 --- a/app/res/layout/entity_select_layout.xml +++ b/app/res/layout/entity_select_layout.xml @@ -53,6 +53,7 @@ android:visibility="gone"/> @@ -63,8 +64,18 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true"/> + + + + notification-channel-push-notifications Required CommCare App is not installed on device Audio Recording Notification + Initializing list… + Processing %1s out of %2s + Finalizing list… diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index e421ca9fc9..7ef93bb27f 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -97,6 +97,8 @@ import org.commcare.tasks.DataPullTask; import org.commcare.tasks.DeleteLogs; import org.commcare.tasks.LogSubmissionTask; +import org.commcare.tasks.PrimeEntityCache; +import org.commcare.tasks.PrimeEntityCacheHelper; import org.commcare.tasks.PurgeStaleArchivedFormsTask; import org.commcare.tasks.templates.ManagedAsyncTask; import org.commcare.update.UpdateHelper; @@ -387,6 +389,7 @@ protected void cancelWorkManagerTasks() { if (currentApp != null) { WorkManager.getInstance(this).cancelUniqueWork( FormSubmissionHelper.getFormSubmissionRequestName(currentApp.getUniqueId())); + PrimeEntityCacheHelper.cancelWork(); } } @@ -796,7 +799,7 @@ public void onServiceConnected(ComponentName className, IBinder service) { purgeLogs(); cleanRawMedia(); - + PrimeEntityCacheHelper.schedulePrimeEntityCacheWorker(); } TimedStatsTracker.registerStartSession(); diff --git a/app/src/org/commcare/activities/EntitySelectActivity.java b/app/src/org/commcare/activities/EntitySelectActivity.java index 7c4fec9b0a..c1f51d0995 100755 --- a/app/src/org/commcare/activities/EntitySelectActivity.java +++ b/app/src/org/commcare/activities/EntitySelectActivity.java @@ -22,7 +22,6 @@ import android.widget.TextView; import android.widget.Toast; -import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.FragmentManager; import com.jakewharton.rxbinding2.widget.AdapterViewItemClickEvent; @@ -58,6 +57,7 @@ import org.commcare.utils.EntityDetailUtils; import org.commcare.utils.EntitySelectRefreshTimer; import org.commcare.utils.SerializationUtil; +import org.commcare.utils.StringUtils; import org.commcare.views.EntityView; import org.commcare.views.TabbedDetailView; import org.commcare.views.UserfacingErrorHandling; @@ -166,6 +166,8 @@ public class EntitySelectActivity extends SaveSessionCommCareActivity // Handler for displaying alert dialog when no location providers are found private final LocationNotificationHandler locationNotificationHandler = new LocationNotificationHandler(this); + private AdapterView visibleView; + private TextView progressTv; @Override public void onCreateSessionSafe(Bundle savedInstanceState) { @@ -254,7 +256,6 @@ private void setupUI(boolean isOrientationChange) { setContentView(R.layout.entity_select_layout); } - AdapterView visibleView; GridView gridView = this.findViewById(R.id.screen_entity_select_grid); ListView listView = this.findViewById(R.id.screen_entity_select_list); if (shortSelect.shouldBeLaidOutInGrid()) { @@ -268,6 +269,7 @@ private void setupUI(boolean isOrientationChange) { gridView.setVisibility(View.GONE); EntitySelectViewSetup.setupDivider(this, listView, shortSelect.usesEntityTileView()); } + progressTv = findViewById(R.id.progress_text); RxAdapterView.itemClickEvents(visibleView) .subscribeOn(AndroidSchedulers.mainThread()) .throttleFirst(CLICK_DEBOUNCE_TIME, TimeUnit.MILLISECONDS) @@ -356,7 +358,7 @@ private void setupUIFromAdapter(AdapterView view) { if (view instanceof ListView) { EntitySelectViewSetup.setupDivider(this, (ListView)view, shortSelect.usesEntityTileView()); } - findViewById(R.id.entity_select_loading).setVisibility(View.GONE); + findViewById(R.id.progress_container).setVisibility(View.GONE); entitySelectSearchUI.setSearchBannerState(); } @@ -476,6 +478,7 @@ public boolean loadEntities() { } if (loader == null && !EntityLoaderTask.attachToActivity(this)) { + setProgressText(StringUtils.getStringRobust(this, R.string.entity_list_initializing)); EntityLoaderTask entityLoader = new EntityLoaderTask(shortSelect, evalContext()); entityLoader.attachListener(this); entityLoader.executeParallel(selectDatum.getNodeset()); @@ -852,16 +855,7 @@ public void deliverLoadResult(List> entities, List references, NodeEntityFactory factory, int focusTargetIndex) { loader = null; - - AdapterView visibleView; - if (shortSelect.shouldBeLaidOutInGrid()) { - visibleView = ((GridView)this.findViewById(R.id.screen_entity_select_grid)); - } else { - ListView listView = this.findViewById(R.id.screen_entity_select_list); - EntitySelectViewSetup.setupDivider(this, listView, shortSelect.usesEntityTileView()); - visibleView = listView; - } - + setProgressText(StringUtils.getStringRobust(this, R.string.entity_list_finalizing)); adapter = new EntityListAdapter(this, shortSelect, references, entities, factory, hideActionsFromEntityList, shortSelect.getCustomActions(evalContext()), inAwesomeMode); visibleView.setAdapter(adapter); @@ -883,7 +877,7 @@ public void deliverLoadResult(List> entities, } } - findViewById(R.id.entity_select_loading).setVisibility(View.GONE); + findViewById(R.id.progress_container).setVisibility(View.GONE); if (adapter != null) { // filter by additional session data (search string, callout result data) @@ -907,6 +901,10 @@ public void deliverLoadResult(List> entities, } } + private void setProgressText(String message) { + progressTv.setText(message); + } + private void restoreAdapterStateFromSession() { entitySelectSearchUI.restoreSearchString(); @@ -933,7 +931,7 @@ private void updateSelectedItem(TreeReference selected, boolean forceMove) { @Override public void attachLoader(EntityLoaderTask task) { - findViewById(R.id.entity_select_loading).setVisibility(View.VISIBLE); + findViewById(R.id.progress_container).setVisibility(View.VISIBLE); this.loader = task; } @@ -995,6 +993,15 @@ public void deliverLoadError(Exception e) { displayCaseListLoadException(e); } + @Override + public void deliverProgress(Integer[] values) { + // throttle to not update text too frequently + if (values[0] % 100 == 0) { + setProgressText(StringUtils.getStringRobust(this, R.string.entity_list_processing, + new String[]{String.valueOf(values[0]), String.valueOf(values[1])})); + } + } + @Override protected boolean onForwardSwipe() { // If user has picked an entity, move along to form entry diff --git a/app/src/org/commcare/entity/AndroidAsyncNodeEntityFactory.kt b/app/src/org/commcare/entity/AndroidAsyncNodeEntityFactory.kt new file mode 100644 index 0000000000..061baa6205 --- /dev/null +++ b/app/src/org/commcare/entity/AndroidAsyncNodeEntityFactory.kt @@ -0,0 +1,90 @@ +package org.commcare.entity + +import android.util.Pair +import org.commcare.cases.entity.AsyncNodeEntityFactory +import org.commcare.cases.entity.Entity +import org.commcare.cases.entity.EntityStorageCache +import org.commcare.suite.model.Detail +import org.commcare.tasks.PrimeEntityCacheHelper +import org.commcare.util.LogTypes +import org.javarosa.core.model.condition.EvaluationContext +import org.javarosa.core.model.instance.TreeReference +import org.javarosa.core.services.Logger + +/** + * Android Specific Implementation of AsyncNodeEntityFactory + * Uses [PrimeEntityCacheHelper] to prime entity cache blocking the user when required + */ +class AndroidAsyncNodeEntityFactory(d: Detail, ec: EvaluationContext?, entityStorageCache: EntityStorageCache?) : + AsyncNodeEntityFactory(d, ec, entityStorageCache), PrimeEntityCacheListener { + + companion object { + const val TWO_MINUTES = 2 * 60 * 1000 + } + + private var cachedEntities: List>? = null + private var completedCachePrime = false + + override fun prepareEntitiesInternal(entities: MutableList>) { + if (detail.shouldCache()) { + // we only want to block if lazy load is not enabled + if (!detail.shouldLazyLoad()) { + val primeEntityCacheHelper = PrimeEntityCacheHelper.getInstance() + if (primeEntityCacheHelper.isInProgress()) { + // if we are priming something else at the moment, expedite the current detail + if (!primeEntityCacheHelper.isDetailInProgress(detail.id)) { + primeEntityCacheHelper.expediteDetailWithId(detail, entities) + } else { + // otherwise wait for existing prime process to complete + primeEntityCacheHelper.setListener(this) + waitForCachePrimeWork(entities, primeEntityCacheHelper) + if (cachedEntities != null) { + entities.clear() + entities.addAll(cachedEntities!!) + } + } + } else { + // we either have not started priming or already completed. In both cases + // we want to re-prime to make sure we calculate any uncalculated data first + primeEntityCacheHelper.primeEntityCacheForDetail(detail, entities) + } + } + } else { + super.prepareEntitiesInternal(entities) + } + } + + private fun waitForCachePrimeWork( + entities: MutableList>, + primeEntityCacheHelper: PrimeEntityCacheHelper + ) { + val startTime = System.currentTimeMillis() + while (!completedCachePrime && (System.currentTimeMillis() - startTime) < TWO_MINUTES) { + // wait for it to be completed + try { + Thread.sleep(100) + } catch (_: InterruptedException) { + } + } + if (!completedCachePrime) { + Logger.log(LogTypes.TYPE_MAINTENANCE, "Still Waiting for cache priming work to complete") + // confirm if we are still priming in the worker. If yes, wait more + // otherwise recall prepareEntitiesInternal to re-evaluate the best thing to do + if (primeEntityCacheHelper.isInProgress() && primeEntityCacheHelper.isDetailInProgress(detail.id)) { + waitForCachePrimeWork(entities, primeEntityCacheHelper) + } else { + prepareEntitiesInternal(entities) + } + } + } + + override fun onPrimeEntityCacheComplete( + currentDetailInProgress: String, + cachedEntitiesWithRefs: Pair>, List> + ) { + if (detail.id!!.contentEquals(currentDetailInProgress)) { + cachedEntities = cachedEntitiesWithRefs.first + completedCachePrime = true + } + } +} diff --git a/app/src/org/commcare/entity/PrimeEntityCacheListener.kt b/app/src/org/commcare/entity/PrimeEntityCacheListener.kt new file mode 100644 index 0000000000..eaeb0e9d19 --- /dev/null +++ b/app/src/org/commcare/entity/PrimeEntityCacheListener.kt @@ -0,0 +1,13 @@ +package org.commcare.entity + +import android.util.Pair +import org.commcare.cases.entity.Entity +import org.javarosa.core.model.instance.TreeReference + +interface PrimeEntityCacheListener { + + fun onPrimeEntityCacheComplete( + currentDetailInProgress: String, + cachedEntitiesWithRefs: Pair>, List> + ) +} diff --git a/app/src/org/commcare/fragments/EntitySubnodeDetailFragment.java b/app/src/org/commcare/fragments/EntitySubnodeDetailFragment.java index a66905956a..f8abe5244c 100755 --- a/app/src/org/commcare/fragments/EntitySubnodeDetailFragment.java +++ b/app/src/org/commcare/fragments/EntitySubnodeDetailFragment.java @@ -103,4 +103,9 @@ public void deliverLoadResult(List> entities, public void deliverLoadError(Exception e) { ((CommCareActivity)getActivity()).displayCaseListLoadException(e); } + + @Override + public void deliverProgress(Integer[] values) { + // nothing to do + } } diff --git a/app/src/org/commcare/tasks/DataPullTask.java b/app/src/org/commcare/tasks/DataPullTask.java index 7f5242b644..761ca366ec 100644 --- a/app/src/org/commcare/tasks/DataPullTask.java +++ b/app/src/org/commcare/tasks/DataPullTask.java @@ -437,6 +437,7 @@ private ResultAndError handleBadLocalState(AndroidTransactionPar if (returnCode == PROGRESS_DONE) { // Recovery was successful onSuccessfulSync(); + PrimeEntityCacheHelper.schedulePrimeEntityCacheWorker(); return new ResultAndError<>(PullTaskResult.DOWNLOAD_SUCCESS); } else if (returnCode == PROGRESS_RECOVERY_FAIL_SAFE || returnCode == PROGRESS_RECOVERY_FAIL_BAD) { wipeLoginIfItOccurred(); diff --git a/app/src/org/commcare/tasks/EntityLoaderHelper.kt b/app/src/org/commcare/tasks/EntityLoaderHelper.kt index d4b40aeaf7..31fb99967f 100644 --- a/app/src/org/commcare/tasks/EntityLoaderHelper.kt +++ b/app/src/org/commcare/tasks/EntityLoaderHelper.kt @@ -7,6 +7,7 @@ import org.commcare.cases.entity.AsyncNodeEntityFactory import org.commcare.cases.entity.Entity import org.commcare.cases.entity.EntityStorageCache import org.commcare.cases.entity.NodeEntityFactory +import org.commcare.entity.AndroidAsyncNodeEntityFactory import org.commcare.models.database.user.models.CommCareEntityStorageCache import org.commcare.preferences.DeveloperPreferences import org.commcare.suite.model.Detail @@ -26,7 +27,7 @@ class EntityLoaderHelper( evalCtx.addFunctionHandler(EntitySelectActivity.getHereFunctionHandler()) if (detail.useAsyncStrategy()) { val entityStorageCache: EntityStorageCache = CommCareEntityStorageCache("case") - factory = AsyncNodeEntityFactory(detail, evalCtx, entityStorageCache) + factory = AndroidAsyncNodeEntityFactory(detail, evalCtx, entityStorageCache) } else { factory = NodeEntityFactory(detail, evalCtx) if (DeveloperPreferences.collectAndDisplayEntityTraces()) { @@ -38,9 +39,12 @@ class EntityLoaderHelper( /** * Loads and prepares a list of entities derived from the given nodeset */ - fun loadEntities(nodeset: TreeReference): Pair>, List>? { + fun loadEntities( + nodeset: TreeReference, + progressListener: EntityLoadingProgressListener + ): Pair>, List>? { val references = factory.expandReferenceList(nodeset) - val entities = loadEntitiesWithReferences(references) + val entities = loadEntitiesWithReferences(references, progressListener) entities?.let { factory.prepareEntities(entities) factory.printAndClearTraces("build") @@ -49,15 +53,32 @@ class EntityLoaderHelper( return null } + /** + * Primes the entity cache + */ + fun cacheEntities(nodeset: TreeReference): Pair>, List> { + val references = factory.expandReferenceList(nodeset) + val entities = loadEntitiesWithReferences(references, null) + cacheEntities(entities) + return Pair>, List>(entities, references) + } + + fun cacheEntities(entities: MutableList>?) { + factory.cacheEntities(entities) + } /** * Loads a list of entities corresponding to the given references */ - private fun loadEntitiesWithReferences(references: List): MutableList>? { + private fun loadEntitiesWithReferences( + references: List, + progressListener: EntityLoadingProgressListener? + ): MutableList>? { val entities: MutableList> = ArrayList() focusTargetIndex = -1 var indexInFullList = 0 - for (ref in references) { + for ((index, ref) in references.withIndex()) { + progressListener?.publishEntityLoadingProgress(index, references.size) if (stopLoading) { return null } diff --git a/app/src/org/commcare/tasks/EntityLoaderListener.java b/app/src/org/commcare/tasks/EntityLoaderListener.java index 6c9d47f2cd..4425957aae 100644 --- a/app/src/org/commcare/tasks/EntityLoaderListener.java +++ b/app/src/org/commcare/tasks/EntityLoaderListener.java @@ -13,4 +13,6 @@ void deliverLoadResult(List> entities, List NodeEntityFactory factory, int focusTargetIndex); void deliverLoadError(Exception e); + + void deliverProgress(Integer... values); } diff --git a/app/src/org/commcare/tasks/EntityLoaderTask.java b/app/src/org/commcare/tasks/EntityLoaderTask.java index 2f4b6ac58d..5b1fb4e97e 100644 --- a/app/src/org/commcare/tasks/EntityLoaderTask.java +++ b/app/src/org/commcare/tasks/EntityLoaderTask.java @@ -18,7 +18,8 @@ * @author ctsims */ public class EntityLoaderTask - extends ManagedAsyncTask>, List>> { + extends ManagedAsyncTask>, List>> implements + EntityLoadingProgressListener { private final static Object lock = new Object(); private static EntityLoaderTask pendingTask = null; @@ -34,7 +35,7 @@ public EntityLoaderTask(Detail detail, EvaluationContext evalCtx) { @Override protected Pair>, List> doInBackground(TreeReference... nodeset) { try { - return entityLoaderHelper.loadEntities(nodeset[0]); + return entityLoaderHelper.loadEntities(nodeset[0], this); } catch (XPathException xe) { XPathErrorLogger.INSTANCE.logErrorToCurrentApp(xe); Logger.exception("Error during EntityLoaderTask: " + ForceCloseLogger.getStackTrace(xe), xe); @@ -112,4 +113,14 @@ protected void onCancelled() { super.onCancelled(); entityLoaderHelper.cancel(); } + + @Override + public void publishEntityLoadingProgress(int progress, int total) { + publishProgress(progress, total); + } + + @Override + protected void onProgressUpdate(Integer... values) { + listener.deliverProgress(values); + } } diff --git a/app/src/org/commcare/tasks/EntityLoadingProgressListener.java b/app/src/org/commcare/tasks/EntityLoadingProgressListener.java new file mode 100644 index 0000000000..f32c746aa0 --- /dev/null +++ b/app/src/org/commcare/tasks/EntityLoadingProgressListener.java @@ -0,0 +1,6 @@ +package org.commcare.tasks; + +public interface EntityLoadingProgressListener { + + void publishEntityLoadingProgress(int progress, int total); +} diff --git a/app/src/org/commcare/tasks/PrimeEntityCache.kt b/app/src/org/commcare/tasks/PrimeEntityCache.kt new file mode 100644 index 0000000000..f6a97aa1f9 --- /dev/null +++ b/app/src/org/commcare/tasks/PrimeEntityCache.kt @@ -0,0 +1,24 @@ +package org.commcare.tasks + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import org.javarosa.core.services.Logger + +class PrimeEntityCache(appContext: Context, workerParams: WorkerParameters) + : Worker(appContext, workerParams) { + + override fun doWork(): Result { + try { + PrimeEntityCacheHelper.getInstance().primeEntityCache() + return Result.success() + } catch (e: Exception) { + Logger.exception("Error while priming cache in worker", e) + } + return Result.failure() + } + + override fun onStopped() { + PrimeEntityCacheHelper.getInstance().cancel() + } +} diff --git a/app/src/org/commcare/tasks/PrimeEntityCacheHelper.kt b/app/src/org/commcare/tasks/PrimeEntityCacheHelper.kt new file mode 100644 index 0000000000..d2a9e572cf --- /dev/null +++ b/app/src/org/commcare/tasks/PrimeEntityCacheHelper.kt @@ -0,0 +1,171 @@ +package org.commcare.tasks + +import android.util.Pair +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import io.reactivex.functions.Cancellable +import org.commcare.CommCareApplication +import org.commcare.cases.entity.Entity +import org.commcare.entity.PrimeEntityCacheListener +import org.commcare.suite.model.Detail +import org.commcare.suite.model.EntityDatum +import org.commcare.sync.FormSubmissionHelper +import org.commcare.utils.AndroidCommCarePlatform +import org.javarosa.core.model.condition.EvaluationContext +import org.javarosa.core.model.instance.TreeReference +import org.javarosa.core.services.Logger + +/** + * Helper to prime cache for all entity screens in the app + * + * Implemented as a singleton to restrict caller from starting another + * cache prime process if one is already in progress. + */ +class PrimeEntityCacheHelper private constructor() : Cancellable { + + private var entityLoaderHelper: EntityLoaderHelper? = null + private var inProgress = false + private var currentDetailInProgress: String? = null + private var listener: PrimeEntityCacheListener? = null + + companion object { + @Volatile + private var instance: PrimeEntityCacheHelper? = null + + const val PRIME_ENTITY_CACHE_REQUEST = "prime-entity-cache-request" + + @JvmStatic + fun getInstance() = + instance ?: synchronized(this) { + instance ?: PrimeEntityCacheHelper().also { instance = it } + } + + /** + * Schedules a background worker request to prime cache for all + * cache backed entity list screens in the current seated app + */ + @JvmStatic + fun schedulePrimeEntityCacheWorker() { + val primeEntityCacheRequest = OneTimeWorkRequest.Builder(PrimeEntityCache::class.java).build() + WorkManager.getInstance(CommCareApplication.instance()) + .enqueueUniqueWork( + PRIME_ENTITY_CACHE_REQUEST, + ExistingWorkPolicy.KEEP, + primeEntityCacheRequest + ) + } + + @JvmStatic + fun cancelWork() { + WorkManager.getInstance(CommCareApplication.instance()).cancelUniqueWork(PRIME_ENTITY_CACHE_REQUEST) + } + } + + /** + * Primes cache for all entity screens in the app + * @throws IllegalStateException if a cache prime is already in progress or user session is not active + */ + fun primeEntityCache() { + checkPreConditions() + primeEntityCacheForApp(CommCareApplication.instance().commCarePlatform) + clearState() + } + + /** + * Primes cache for given entities set against the [detail] + * @throws IllegalStateException if a cache prime is already in progress or user session is not active + */ + fun primeEntityCacheForDetail( + detail: Detail, + entities: MutableList> + ) { + checkPreConditions() + primeCacheForDetail(detail, null, entities) + clearState() + } + + /** + * Cancel any current cache prime process to expedite cache calculations for given [detail] + * Reschedules the work again in background afterwards + */ + fun expediteDetailWithId(detail: Detail, entities: MutableList>) { + cancel() + primeEntityCacheForDetail(detail, entities) + schedulePrimeEntityCacheWorker() + } + + fun isDetailInProgress(detailId: String): Boolean { + return currentDetailInProgress?.contentEquals(detailId) ?: false + } + + private fun primeEntityCacheForApp(commCarePlatform: AndroidCommCarePlatform) { + inProgress = true + val commandMap = commCarePlatform.commandToEntryMap + for (command in commandMap.keys()) { + val entry = commandMap[command]!! + val sessionDatums = entry.sessionDataReqs + for (sessionDatum in sessionDatums) { + if (sessionDatum is EntityDatum) { + val shortDetailId = sessionDatum.shortDetail + if (shortDetailId != null) { + val detail = commCarePlatform.getDetail(shortDetailId) + primeCacheForDetail(detail, sessionDatum) + } + } + } + } + } + + private fun primeCacheForDetail(detail: Detail, sessionDatum: EntityDatum? = null, entities: MutableList>? = null) { + if (!detail.shouldCache()) return + currentDetailInProgress = detail.id + entityLoaderHelper = EntityLoaderHelper(detail, evalCtx()) + + // Handle the cache operation based on the available input + val cachedEntitiesWithRefs = when { + sessionDatum != null -> entityLoaderHelper!!.cacheEntities(sessionDatum.nodeset) + entities != null -> { + entityLoaderHelper!!.cacheEntities(entities) + Pair(entities, null) as Pair>, List> + } + else -> return + } + + // Call the listener with the appropriate result + listener?.onPrimeEntityCacheComplete(currentDetailInProgress!!, + cachedEntitiesWithRefs + ) + currentDetailInProgress = null + } + + private fun evalCtx(): EvaluationContext { + return CommCareApplication.instance().currentSessionWrapper.evaluationContext + } + + private fun clearState() { + entityLoaderHelper = null + inProgress = false + listener = null + currentDetailInProgress = null + instance = null + } + + private fun checkPreConditions() { + require(CommCareApplication.instance().session.isActive) { "User session must be active to prime entity cache" } + require(!inProgress) { "We are already priming the cache" } + } + + override fun cancel() { + entityLoaderHelper?.cancel() + clearState() + } + + fun isInProgress(): Boolean { + return inProgress + } + + fun setListener(primeEntityCacheListener: PrimeEntityCacheListener) { + listener = primeEntityCacheListener + } +}