diff options
author | Stefan Niedermann <info@niedermann.it> | 2021-09-30 13:36:14 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2021-09-30 13:36:14 +0300 |
commit | 5801f00bfb79b19b02dc8fcb6862ed8a7bb71a1d (patch) | |
tree | 83a657452dbc8d1ee7c096a96c67d2de6d915e97 /app/src/main | |
parent | 411c61c21dd6a0f9df93da62e38d837d2de7b66d (diff) |
#761 Import notes one by one to avoid read timeout for the first sync
Signed-off-by: Stefan Niedermann <info@niedermann.it>
Diffstat (limited to 'app/src/main')
12 files changed, 213 insertions, 19 deletions
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java index 1e50ac0f..54e92baf 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java @@ -96,7 +96,7 @@ public class ImportAccountActivity extends AppCompatActivity { Log.i(TAG, "Loading capabilities for " + ssoAccount.name); final var capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null, ApiProvider.getInstance()); final String displayName = CapabilitiesClient.getDisplayName(getApplicationContext(), ssoAccount, ApiProvider.getInstance()); - importAccountViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, displayName, new IResponseCallback<Account>() { + final var status$ = importAccountViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, displayName, new IResponseCallback<>() { /** * Update syncing when adding account @@ -123,6 +123,16 @@ public class ImportAccountActivity extends AppCompatActivity { }); } }); + runOnUiThread(() -> status$.observe(ImportAccountActivity.this, (status) -> { + binding.progressText.setVisibility(View.VISIBLE); + Log.v(TAG, "Status: " + status.count + " of " + status.total); + if(status.count > 0) { + binding.progressCircular.setIndeterminate(false); + } + binding.progressText.setText(getString(R.string.progress_import, status.count + 1, status.total)); + binding.progressCircular.setProgress(status.count); + binding.progressCircular.setMax(status.total); + })); } catch (Throwable t) { t.printStackTrace(); ApiProvider.getInstance().invalidateAPICache(ssoAccount); @@ -162,6 +172,7 @@ public class ImportAccountActivity extends AppCompatActivity { runOnUiThread(() -> { binding.addButton.setEnabled(true); binding.progressCircular.setVisibility(View.GONE); + binding.progressText.setVisibility(View.GONE); }); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java index 70b3b565..1d60a043 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java @@ -11,11 +11,10 @@ import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.shared.model.Capabilities; import it.niedermann.owncloud.notes.shared.model.IResponseCallback; +import it.niedermann.owncloud.notes.shared.model.ImportStatus; public class ImportAccountViewModel extends AndroidViewModel { - private static final String TAG = ImportAccountViewModel.class.getSimpleName(); - @NonNull private final NotesRepository repo; @@ -24,7 +23,7 @@ public class ImportAccountViewModel extends AndroidViewModel { this.repo = NotesRepository.getInstance(application); } - public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback<Account> callback) { - repo.addAccount(url, username, accountName, capabilities, displayName, callback); + public LiveData<ImportStatus> addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback<Account> callback) { + return repo.addAccount(url, username, accountName, capabilities, displayName, callback); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index ee079fbd..132acc9c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -668,27 +668,41 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A AccountImporter.onActivityResult(requestCode, resultCode, data, this, (ssoAccount) -> { CapabilitiesWorker.update(this); executor.submit(() -> { + final var importSnackbar = BrandedSnackbar.make(binding.drawerLayout, R.string.progress_import_indeterminate, Snackbar.LENGTH_INDEFINITE); Log.i(TAG, "Added account: " + "name:" + ssoAccount.name + ", " + ssoAccount.url + ", userId" + ssoAccount.userId); try { Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name); final var capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null, ApiProvider.getInstance()); final String displayName = CapabilitiesClient.getDisplayName(getApplicationContext(), ssoAccount, ApiProvider.getInstance()); - mainViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, displayName, new IResponseCallback<Account>() { + final var status$ = mainViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, displayName, new IResponseCallback<Account>() { @Override public void onSuccess(Account result) { executor.submit(() -> { + runOnUiThread(() -> { + importSnackbar.setText(R.string.account_imported); + importSnackbar.setAction(R.string.simple_switch, (v) -> mainViewModel.postCurrentAccount(mainViewModel.getLocalAccountByAccountName(ssoAccount.name))); + }); Log.i(TAG, capabilities.toString()); - final var a = mainViewModel.getLocalAccountByAccountName(ssoAccount.name); - runOnUiThread(() -> mainViewModel.postCurrentAccount(a)); }); } @Override public void onError(@NonNull Throwable t) { - runOnUiThread(() -> ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + runOnUiThread(() -> { + importSnackbar.dismiss(); + ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + }); } }); + runOnUiThread(() -> status$.observe(this, (status) -> { + importSnackbar.show(); + Log.v(TAG, "Status: " + status.count + " of " + status.total); + if(status.count > 0) { + importSnackbar.setText(getString(R.string.progress_import, status.count + 1, status.total)); + } + })); } catch (Throwable e) { + importSnackbar.dismiss(); ApiProvider.getInstance().invalidateAPICache(ssoAccount); // Happens when importing an already existing account the second time if (e instanceof TokenMismatchException && mainViewModel.getLocalAccountByAccountName(ssoAccount.name) != null) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java index e7cf1669..b52fdc79 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java @@ -46,6 +46,7 @@ import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData; import it.niedermann.owncloud.notes.shared.model.Capabilities; import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod; import it.niedermann.owncloud.notes.shared.model.IResponseCallback; +import it.niedermann.owncloud.notes.shared.model.ImportStatus; import it.niedermann.owncloud.notes.shared.model.Item; import it.niedermann.owncloud.notes.shared.model.NavigationCategory; @@ -538,8 +539,8 @@ public class MainViewModel extends AndroidViewModel { }); } - public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback<Account> callback) { - repo.addAccount(url, username, accountName, capabilities, displayName, callback); + public LiveData<ImportStatus> addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback<Account> callback) { + return repo.addAccount(url, username, accountName, capabilities, displayName, callback); } public LiveData<Note> getFullNote$(long id) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesImportTask.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesImportTask.java new file mode 100644 index 00000000..acdf1441 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesImportTask.java @@ -0,0 +1,84 @@ +package it.niedermann.owncloud.notes.persistence; + +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.nextcloud.android.sso.AccountImporter; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import it.niedermann.owncloud.notes.persistence.entity.Account; +import it.niedermann.owncloud.notes.persistence.sync.NotesAPI; +import it.niedermann.owncloud.notes.shared.model.IResponseCallback; +import it.niedermann.owncloud.notes.shared.model.ImportStatus; +import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil; + + +public class NotesImportTask { + + private static final String TAG = NotesImportTask.class.getSimpleName(); + + private final NotesAPI notesAPI; + @NonNull + private final NotesRepository repo; + @NonNull + private final Account localAccount; + @NonNull + private final ExecutorService executor; + @NonNull + private final ExecutorService fetchExecutor; + + NotesImportTask(@NonNull Context context, @NonNull NotesRepository repo, @NonNull Account localAccount, @NonNull ExecutorService executor, @NonNull ApiProvider apiProvider) throws NextcloudFilesAppAccountNotFoundException { + this(context, repo, localAccount, executor, Executors.newFixedThreadPool(20), apiProvider); + } + + private NotesImportTask(@NonNull Context context, @NonNull NotesRepository repo, @NonNull Account localAccount, @NonNull ExecutorService executor, @NonNull ExecutorService fetchExecutor, @NonNull ApiProvider apiProvider) throws NextcloudFilesAppAccountNotFoundException { + this.repo = repo; + this.localAccount = localAccount; + this.executor = executor; + this.fetchExecutor = fetchExecutor; + this.notesAPI = apiProvider.getNotesAPI(context, AccountImporter.getSingleSignOnAccount(context, localAccount.getAccountName()), ApiVersionUtil.getPreferredApiVersion(localAccount.getApiVersion())); + } + + public LiveData<ImportStatus> importNotes(@NonNull IResponseCallback<Void> callback) { + final var status$ = new MutableLiveData<ImportStatus>(); + Log.i(TAG, "STARTING IMPORT"); + executor.submit(() -> { + Log.i(TAG, "… Fetching notes IDs"); + final var status = new ImportStatus(); + final var remoteIds = notesAPI.getNotesIDs().blockingSingle(); + status.total = remoteIds.size(); + status$.postValue(status); + Log.i(TAG, "… Total count: " + remoteIds.size()); + final var latch = new CountDownLatch(remoteIds.size()); + for (long id : remoteIds) { + fetchExecutor.submit(() -> { + try { + repo.addNote(localAccount.getId(), notesAPI.getNote(id).blockingSingle().getResponse()); + } catch (Throwable t) { + Log.w(TAG, "Could not import note with remoteId " + id + ": " + t.getMessage()); + status.warnings.add(t); + } + status.count++; + status$.postValue(status); + latch.countDown(); + }); + } + try { + latch.await(); + Log.i(TAG, "IMPORT FINISHED"); + callback.onSuccess(null); + } catch (InterruptedException e) { + callback.onError(e); + } + }); + return status$; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java index 59eafa05..8963d87f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java @@ -58,6 +58,7 @@ import it.niedermann.owncloud.notes.shared.model.DBStatus; import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType; import it.niedermann.owncloud.notes.shared.model.IResponseCallback; import it.niedermann.owncloud.notes.shared.model.ISyncCallback; +import it.niedermann.owncloud.notes.shared.model.ImportStatus; import it.niedermann.owncloud.notes.shared.model.NavigationCategory; import it.niedermann.owncloud.notes.shared.model.NotesSettings; import it.niedermann.owncloud.notes.shared.model.SyncResultStatus; @@ -86,6 +87,7 @@ public class NotesRepository { private final ApiProvider apiProvider; private final ExecutorService executor; private final ExecutorService syncExecutor; + private final ExecutorService importExecutor; private final Context context; private final NotesDatabase db; private final String defaultNonEmptyTitle; @@ -138,16 +140,17 @@ public class NotesRepository { public static synchronized NotesRepository getInstance(@NonNull Context context) { if (instance == null) { - instance = new NotesRepository(context, NotesDatabase.getInstance(context.getApplicationContext()), Executors.newCachedThreadPool(), Executors.newSingleThreadExecutor(), ApiProvider.getInstance()); + instance = new NotesRepository(context, NotesDatabase.getInstance(context.getApplicationContext()), Executors.newCachedThreadPool(), Executors.newSingleThreadExecutor(), Executors.newSingleThreadExecutor(), ApiProvider.getInstance()); } return instance; } - private NotesRepository(@NonNull final Context context, @NonNull final NotesDatabase db, @NonNull final ExecutorService executor, @NonNull final ExecutorService syncExecutor, @NonNull ApiProvider apiProvider) { + private NotesRepository(@NonNull final Context context, @NonNull final NotesDatabase db, @NonNull final ExecutorService executor, @NonNull final ExecutorService syncExecutor, @NonNull final ExecutorService importExecutor, @NonNull ApiProvider apiProvider) { this.context = context.getApplicationContext(); this.db = db; this.executor = executor; this.syncExecutor = syncExecutor; + this.importExecutor = importExecutor; this.apiProvider = apiProvider; this.defaultNonEmptyTitle = NoteUtil.generateNonEmptyNoteTitle("", this.context); this.syncOnlyOnWifiKey = context.getApplicationContext().getResources().getString(R.string.pref_key_wifi_only); @@ -166,13 +169,36 @@ public class NotesRepository { // Accounts @AnyThread - public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback<Account> callback) { - final var createdAccount = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account(url, username, accountName, displayName, capabilities))); - if (createdAccount == null) { + public LiveData<ImportStatus> addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback<Account> callback) { + final var account = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account(url, username, accountName, displayName, capabilities))); + if (account == null) { callback.onError(new Exception("Could not read created account.")); } else { - callback.onSuccess(createdAccount); + if (isSyncPossible()) { + syncActive.put(account.getId(), true); + try { + Log.d(TAG, "... starting now"); + final NotesImportTask importTask = new NotesImportTask(context, this, account, importExecutor, apiProvider); + return importTask.importNotes(new IResponseCallback<>() { + @Override + public void onSuccess(Void result) { + callback.onSuccess(account); + } + + @Override + public void onError(@NonNull Throwable t) { + callback.onError(t); + } + }); + } catch (NextcloudFilesAppAccountNotFoundException e) { + Log.e(TAG, "... Could not find " + SingleSignOnAccount.class.getSimpleName() + " for account name " + account.getAccountName()); + callback.onError(e); + } + } else { + callback.onError(new NetworkErrorException()); + } } + return new MutableLiveData<>(new ImportStatus()); } @WorkerThread diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java index 3e552ae6..4f8bee92 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java @@ -12,6 +12,7 @@ import com.nextcloud.android.sso.api.ParsedResponse; import java.util.Calendar; import java.util.List; +import java.util.stream.Collectors; import io.reactivex.Observable; import it.niedermann.owncloud.notes.persistence.entity.Note; @@ -69,6 +70,26 @@ public class NotesAPI { } } + public Observable<List<Long>> getNotesIDs() { + if (ApiVersion.API_VERSION_1_0.equals(usedApiVersion)) { + return notesAPI_1_0.getNotesIDs().map(response -> response.getResponse().stream().map(Note::getRemoteId).collect(Collectors.toList())); + } else if (ApiVersion.API_VERSION_0_2.equals(usedApiVersion)) { + return notesAPI_0_2.getNotesIDs().map(response -> response.getResponse().stream().map(Note::getRemoteId).collect(Collectors.toList())); + } else { + throw new UnsupportedOperationException("Used API version " + usedApiVersion + " does not support getNotesIDs()."); + } + } + + public Observable<ParsedResponse<Note>> getNote(long remoteId) { + if (ApiVersion.API_VERSION_1_0.equals(usedApiVersion)) { + return notesAPI_1_0.getNote(remoteId); + } else if (ApiVersion.API_VERSION_0_2.equals(usedApiVersion)) { + return notesAPI_0_2.getNote(remoteId); + } else { + throw new UnsupportedOperationException("Used API version " + usedApiVersion + " does not support getNote()."); + } + } + public Call<Note> createNote(Note note) { if (ApiVersion.API_VERSION_1_0.equals(usedApiVersion)) { return notesAPI_1_0.createNote(note); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_0_2.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_0_2.java index fd642064..13d66c03 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_0_2.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_0_2.java @@ -25,9 +25,15 @@ public interface NotesAPI_0_2 { @GET("notes") Observable<ParsedResponse<List<Note>>> getNotes(@Query("pruneBefore") long lastModified, @Header("If-None-Match") String lastETag); + @GET("notes?exclude=etag,readonly,content,title,category,favorite,modified") + Observable<ParsedResponse<List<Note>>> getNotesIDs(); + @POST("notes") Call<Note> createNote(@Body NotesAPI.Note_0_2 note); + @GET("notes/{remoteId}") + Observable<ParsedResponse<Note>> getNote(@Path("remoteId") long remoteId); + @PUT("notes/{remoteId}") Call<Note> editNote(@Body NotesAPI.Note_0_2 note, @Path("remoteId") long remoteId); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_1_0.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_1_0.java index dfc176c3..20f6f9a7 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_1_0.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_1_0.java @@ -26,9 +26,15 @@ public interface NotesAPI_1_0 { @GET("notes") Observable<ParsedResponse<List<Note>>> getNotes(@Query("pruneBefore") long lastModified, @Header("If-None-Match") String lastETag); + @GET("notes?exclude=etag,readonly,content,title,category,favorite,modified") + Observable<ParsedResponse<List<Note>>> getNotesIDs(); + @POST("notes") Call<Note> createNote(@Body Note note); + @GET("notes/{remoteId}") + Observable<ParsedResponse<Note>> getNote(@Path("remoteId") long remoteId); + @PUT("notes/{remoteId}") Call<Note> editNote(@Body Note note, @Path("remoteId") long remoteId); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ImportStatus.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ImportStatus.java new file mode 100644 index 00000000..7ae189ef --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ImportStatus.java @@ -0,0 +1,10 @@ +package it.niedermann.owncloud.notes.shared.model; + +import java.util.Collection; +import java.util.LinkedList; + +public class ImportStatus { + public int count = 0; + public int total = 0; + public final Collection<Throwable> warnings = new LinkedList<>(); +} diff --git a/app/src/main/res/layout/activity_import_account.xml b/app/src/main/res/layout/activity_import_account.xml index ed072796..7484a61b 100644 --- a/app/src/main/res/layout/activity_import_account.xml +++ b/app/src/main/res/layout/activity_import_account.xml @@ -65,12 +65,24 @@ <ProgressBar android:id="@+id/progress_circular" - android:layout_width="wrap_content" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/add_button" android:layout_centerHorizontal="true" - android:layout_marginTop="32dp" + android:layout_marginTop="@dimen/spacer_5x" + android:indeterminate="true" android:indeterminateTint="@color/defaultBrand" android:visibility="gone" /> + + <TextView + android:id="@+id/progress_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/progress_circular" + android:layout_centerHorizontal="true" + android:layout_marginTop="@dimen/spacer_2x" + android:visibility="gone" + tools:text="@string/progress_import_indeterminate" /> </RelativeLayout> </ScrollView>
\ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 772a57fe..693946ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ <string name="action_search">Search</string> <string name="action_sorting_method">Sorting method</string> <string name="simple_cancel">Cancel</string> + <string name="simple_switch">Switch</string> <string name="simple_edit">Edit</string> <string name="simple_remove">Remove</string> <string name="action_edit_save">Save</string> @@ -315,4 +316,7 @@ <string name="settings_file_suffix_description">File extension for new notes in your Nextcloud</string> <string name="settings_file_suffix_success">New file suffix: %1$s</string> <string name="http_status_code">HTTP status code: %1$d</string> + <string name="progress_import_indeterminate">Importing notes…</string> + <string name="progress_import">Importing note %1$d of %2$d…</string> + <string name="account_imported">Account imported.</string> </resources> |