From 0b92c6ce0d687cac88c805087bb13aa92d876879 Mon Sep 17 00:00:00 2001 From: Helium314 Date: Thu, 7 Sep 2023 09:19:47 +0200 Subject: [PATCH] remove old unused dictionary pack settings --- app/src/debug/res/values/dictionary-pack.xml | 24 - app/src/main/AndroidManifest.xml | 3 +- .../inputmethod/dictionarypack/ActionBatch.kt | 502 ---------- .../dictionarypack/BadFormatException.kt | 9 - .../dictionarypack/ButtonSwitcher.kt | 135 --- .../dictionarypack/CommonPreferences.kt | 23 - .../DictionaryListInterfaceState.kt | 64 -- .../dictionarypack/DictionaryProvider.kt | 440 --------- .../DictionarySettingsFragment.kt | 224 ----- .../dictionarypack/LogProblemReporter.kt | 13 - .../dictionarypack/MetadataDbHelper.kt | 935 ------------------ .../dictionarypack/MetadataHandler.kt | 141 --- .../dictionarypack/MetadataParser.kt | 85 -- .../dictionarypack/MetadataUriGetter.kt | 12 - .../inputmethod/dictionarypack/PrivateLog.kt | 69 -- .../dictionarypack/ProblemReporter.kt | 8 - .../dictionarypack/WordListMetadata.kt | 75 -- .../dictionarypack/WordListPreference.kt | 198 ---- app/src/main/res/layout/dictionary_line.xml | 111 --- app/src/main/res/values/dictionary-pack.xml | 3 - 20 files changed, 2 insertions(+), 3072 deletions(-) delete mode 100644 app/src/debug/res/values/dictionary-pack.xml delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ActionBatch.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/BadFormatException.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ButtonSwitcher.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/CommonPreferences.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryListInterfaceState.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryProvider.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsFragment.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/LogProblemReporter.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataDbHelper.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataHandler.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataParser.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataUriGetter.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/PrivateLog.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ProblemReporter.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListMetadata.kt delete mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListPreference.kt delete mode 100644 app/src/main/res/layout/dictionary_line.xml diff --git a/app/src/debug/res/values/dictionary-pack.xml b/app/src/debug/res/values/dictionary-pack.xml deleted file mode 100644 index 790ab5aa1..000000000 --- a/app/src/debug/res/values/dictionary-pack.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - org.dslul.openboard.inputmethod.latin.debug - - org.dslul.openboard.inputmethod.debug.dictionarypack.aosp - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8092e3845..5285bf069 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -160,12 +160,13 @@ + diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ActionBatch.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ActionBatch.kt deleted file mode 100644 index da36c128e..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ActionBatch.kt +++ /dev/null @@ -1,502 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.content.ContentValues -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.text.TextUtils -import android.util.Log -import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.DisableAction -import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.EnableAction -import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.FinishDeleteAction -import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.ForgetAction -import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.MakeAvailableAction -import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.MarkPreInstalledAction -import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.StartDeleteAction -import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.UpdateDataAction -import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils -import java.util.* - -/** - * Object representing an upgrade from one state to another. - * - * This implementation basically encapsulates a list of Runnable objects. In the future - * it may manage dependencies between them. Concretely, it does not use Runnable because the - * actions need an argument. - */ -/* - -The state of a word list follows the following scheme. - - | ^ - MakeAvailable | - | .------------Forget--------' - V | - STATUS_AVAILABLE <-------------------------. - | | -StartDownloadAction FinishDeleteAction - | | - V | -STATUS_DOWNLOADING EnableAction-- STATUS_DELETING - | | ^ -InstallAfterDownloadAction | | - | .---------------' StartDeleteAction - | | | - V V | - STATUS_INSTALLED <--EnableAction-- STATUS_DISABLED - --DisableAction--> - - It may also be possible that DisableAction or StartDeleteAction or - DownloadAction run when the file is still downloading. This cancels - the download and returns to STATUS_AVAILABLE. - Also, an UpdateDataAction may apply in any state. It does not affect - the state in any way (nor type, local filename, id or version) but - may update other attributes like description or remote filename. - - Forget is an DB maintenance action that removes the entry if it is not installed or disabled. - This happens when the word list information disappeared from the server, or when a new version - is available and we should forget about the old one. -*/ -class ActionBatch { - /** - * A piece of update. - * - * Action is basically like a Runnable that takes an argument. - */ - interface Action { - /** - * Execute this action NOW. - * @param context the context to get system services, resources, databases - */ - fun execute(context: Context?) - } - - /** - * An action that enables an existing word list. - */ - class EnableAction(clientId: String, wordList: WordListMetadata?) : Action { - private val mClientId: String - // The state to upgrade from. May not be null. - val mWordList: WordListMetadata? - - override fun execute(context: Context?) { - if (null == mWordList) { - Log.e(TAG, "EnableAction with a null parameter!") - return - } - DebugLogUtils.l("Enabling word list") - val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId) - val values: ContentValues = MetadataDbHelper.getContentValuesByWordListId(db, - mWordList.mId, mWordList.mVersion)!! - val status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN) - if (MetadataDbHelper.STATUS_DISABLED != status - && MetadataDbHelper.STATUS_DELETING != status) { - Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + " : " + status - + " for an enable action. Cancelling") - return - } - MetadataDbHelper.markEntryAsEnabled(db, mWordList.mId, mWordList.mVersion) - } - - companion object { - val TAG = "DictionaryProvider:" + EnableAction::class.java.simpleName - } - - init { - DebugLogUtils.l("New EnableAction for client ", clientId, " : ", wordList) - mClientId = clientId - mWordList = wordList - } - } - - /** - * An action that disables a word list. - */ - class DisableAction(clientId: String, wordlist: WordListMetadata?) : Action { - private val mClientId: String - // The word list to disable. May not be null. - val mWordList: WordListMetadata? - - override fun execute(context: Context?) { - if (null == mWordList) { // This should never happen - Log.e(TAG, "DisableAction with a null word list!") - return - } - DebugLogUtils.l("Disabling word list : $mWordList") - val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId) - val values: ContentValues = MetadataDbHelper.getContentValuesByWordListId(db, - mWordList.mId, mWordList.mVersion)!! - val status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN) - if (MetadataDbHelper.STATUS_INSTALLED == status) { // Disabling an installed word list - MetadataDbHelper.markEntryAsDisabled(db, mWordList.mId, mWordList.mVersion) - } else { - if (MetadataDbHelper.STATUS_DOWNLOADING != status) { - Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' : " - + status + " for a disable action. Fall back to marking as available.") - } - // The word list is still downloading. Cancel the download and revert the -// word list status to "available". - } - } - - companion object { - val TAG = "DictionaryProvider:" + DisableAction::class.java.simpleName - } - - init { - DebugLogUtils.l("New Disable action for client ", clientId, " : ", wordlist) - mClientId = clientId - mWordList = wordlist - } - } - - /** - * An action that makes a word list available. - */ - class MakeAvailableAction(clientId: String, wordlist: WordListMetadata?) : Action { - private val mClientId: String - // The word list to make available. May not be null. - val mWordList: WordListMetadata? - - override fun execute(context: Context?) { - if (null == mWordList) { // This should never happen - Log.e(TAG, "MakeAvailableAction with a null word list!") - return - } - val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId) - if (null != MetadataDbHelper.getContentValuesByWordListId(db, - mWordList.mId, mWordList.mVersion)) { - Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' " - + " for a makeavailable action. Marking as available anyway.") - } - DebugLogUtils.l("Making word list available : $mWordList") - // If mLocalFilename is null, then it's a remote file that hasn't been downloaded -// yet, so we set the local filename to the empty string. - val values: ContentValues = MetadataDbHelper.makeContentValues(0, - MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_AVAILABLE, - mWordList.mId, mWordList.mLocale, mWordList.mDescription, - mWordList.mLocalFilename ?: "", - mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, - mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize, - mWordList.mVersion, mWordList.mFormatVersion) - PrivateLog.log("Insert 'available' record for " + mWordList.mDescription - + " and locale " + mWordList.mLocale) - db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values) - } - - companion object { - val TAG = "DictionaryProvider:" + MakeAvailableAction::class.java.simpleName - } - - init { - DebugLogUtils.l("New MakeAvailable action", clientId, " : ", wordlist) - mClientId = clientId - mWordList = wordlist - } - } - - /** - * An action that marks a word list as pre-installed. - * - * This is almost the same as MakeAvailableAction, as it only inserts a line with parameters - * received from outside. - * Unlike MakeAvailableAction, the parameters are not received from a downloaded metadata file - * but from the client directly; it marks a word list as being "installed" and not "available". - * It also explicitly sets the filename to the empty string, so that we don't try to open - * it on our side. - */ - class MarkPreInstalledAction(clientId: String?, wordlist: WordListMetadata?) : Action { - private val mClientId: String? - // The word list to mark pre-installed. May not be null. - val mWordList: WordListMetadata? - - override fun execute(context: Context?) { - if (null == mWordList) { // This should never happen - Log.e(TAG, "MarkPreInstalledAction with a null word list!") - return - } - val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId) - if (null != MetadataDbHelper.getContentValuesByWordListId(db, - mWordList.mId, mWordList.mVersion)) { - Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' " - + " for a markpreinstalled action. Marking as preinstalled anyway.") - } - DebugLogUtils.l("Marking word list preinstalled : $mWordList") - // This word list is pre-installed : we don't have its file. We should reset -// the local file name to the empty string so that we don't try to open it -// accidentally. The remote filename may be set by the application if it so wishes. - val values: ContentValues = MetadataDbHelper.makeContentValues(0, - MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_INSTALLED, - mWordList.mId, mWordList.mLocale, mWordList.mDescription, - if (TextUtils.isEmpty(mWordList.mLocalFilename)) "" else mWordList.mLocalFilename, - mWordList.mRemoteFilename, mWordList.mLastUpdate, - mWordList.mRawChecksum, mWordList.mChecksum, mWordList.mRetryCount, - mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion) - PrivateLog.log("Insert 'preinstalled' record for " + mWordList.mDescription - + " and locale " + mWordList.mLocale) - db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values) - } - - companion object { - val TAG = ("DictionaryProvider:" - + MarkPreInstalledAction::class.java.simpleName) - } - - init { - DebugLogUtils.l("New MarkPreInstalled action", clientId, " : ", wordlist) - mClientId = clientId - mWordList = wordlist - } - } - - /** - * An action that updates information about a word list - description, locale etc - */ - class UpdateDataAction(clientId: String, wordlist: WordListMetadata?) : Action { - private val mClientId: String - val mWordList: WordListMetadata? - override fun execute(context: Context?) { - if (null == mWordList) { // This should never happen - Log.e(TAG, "UpdateDataAction with a null word list!") - return - } - val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId) - val oldValues: ContentValues = MetadataDbHelper.getContentValuesByWordListId(db, - mWordList.mId, mWordList.mVersion)!! - if (null == oldValues) { - Log.e(TAG, "Trying to update data about a non-existing word list. Bailing out.") - return - } - DebugLogUtils.l("Updating data about a word list : $mWordList") - val values: ContentValues = MetadataDbHelper.makeContentValues( - oldValues.getAsInteger(MetadataDbHelper.PENDINGID_COLUMN), - oldValues.getAsInteger(MetadataDbHelper.TYPE_COLUMN), - oldValues.getAsInteger(MetadataDbHelper.STATUS_COLUMN), - mWordList.mId, mWordList.mLocale, mWordList.mDescription, - oldValues.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN), - mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, - mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize, - mWordList.mVersion, mWordList.mFormatVersion) - PrivateLog.log("Updating record for " + mWordList.mDescription - + " and locale " + mWordList.mLocale) - db.update(MetadataDbHelper.METADATA_TABLE_NAME, values, - MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND " - + MetadataDbHelper.VERSION_COLUMN + " = ?", arrayOf(mWordList.mId, Integer.toString(mWordList.mVersion))) - } - - companion object { - val TAG = "DictionaryProvider:" + UpdateDataAction::class.java.simpleName - } - - init { - DebugLogUtils.l("New UpdateData action for client ", clientId, " : ", wordlist) - mClientId = clientId - mWordList = wordlist - } - } - - /** - * An action that deletes the metadata about a word list if possible. - * - * This is triggered when a specific word list disappeared from the server, or when a fresher - * word list is available and the old one was not installed. - * If the word list has not been installed, it's possible to delete its associated metadata. - * Otherwise, the settings are retained so that the user can still administrate it. - */ - class ForgetAction(clientId: String, wordlist: WordListMetadata?, - hasNewerVersion: Boolean) : Action { - private val mClientId: String - // The word list to remove. May not be null. - val mWordList: WordListMetadata? - val mHasNewerVersion: Boolean - override fun execute(context: Context?) { - if (null == mWordList) { // This should never happen - Log.e(TAG, "TryRemoveAction with a null word list!") - return - } - DebugLogUtils.l("Trying to remove word list : $mWordList") - val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId) - val values: ContentValues = MetadataDbHelper.getContentValuesByWordListId(db, - mWordList.mId, mWordList.mVersion)!! - if (null == values) { - Log.e(TAG, "Trying to update the metadata of a non-existing wordlist. Cancelling.") - return - } - val status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN) - if (mHasNewerVersion && MetadataDbHelper.STATUS_AVAILABLE != status) { // If we have a newer version of this word list, we should be here ONLY if it was -// not installed - else we should be upgrading it. - Log.e(TAG, "Unexpected status for forgetting a word list info : " + status - + ", removing URL to prevent re-download") - } - if (MetadataDbHelper.STATUS_INSTALLED == status || MetadataDbHelper.STATUS_DISABLED == status || MetadataDbHelper.STATUS_DELETING == status) { // If it is installed or disabled, we need to mark it as deleted so that LatinIME -// will remove it next time it enquires for dictionaries. -// If it is deleting and we don't have a new version, then we have to wait until -// LatinIME actually has deleted it before we can remove its metadata. -// In both cases, remove the URI from the database since it is not supposed to -// be accessible any more. - values.put(MetadataDbHelper.REMOTE_FILENAME_COLUMN, "") - values.put(MetadataDbHelper.STATUS_COLUMN, MetadataDbHelper.STATUS_DELETING) - db.update(MetadataDbHelper.METADATA_TABLE_NAME, values, - MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND " - + MetadataDbHelper.VERSION_COLUMN + " = ?", arrayOf(mWordList.mId, Integer.toString(mWordList.mVersion))) - } else { // If it's AVAILABLE or DOWNLOADING or even UNKNOWN, delete the entry. - db.delete(MetadataDbHelper.METADATA_TABLE_NAME, - MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND " - + MetadataDbHelper.VERSION_COLUMN + " = ?", arrayOf(mWordList.mId, Integer.toString(mWordList.mVersion))) - } - } - - companion object { - val TAG = "DictionaryProvider:" + ForgetAction::class.java.simpleName - } - - init { - DebugLogUtils.l("New TryRemove action for client ", clientId, " : ", wordlist) - mClientId = clientId - mWordList = wordlist - mHasNewerVersion = hasNewerVersion - } - } - - /** - * An action that sets the word list for deletion as soon as possible. - * - * This is triggered when the user requests deletion of a word list. This will mark it as - * deleted in the database, and fire an intent for Android Keyboard to take notice and - * reload its dictionaries right away if it is up. If it is not up now, then it will - * delete the actual file the next time it gets up. - * A file marked as deleted causes the content provider to supply a zero-sized file to - * Android Keyboard, which will overwrite any existing file and provide no words for this - * word list. This is not exactly a "deletion", since there is an actual file which takes up - * a few bytes on the disk, but this allows to override a default dictionary with an empty - * dictionary. This way, there is no need for the user to make a distinction between - * dictionaries installed by default and add-on dictionaries. - */ - class StartDeleteAction(clientId: String, wordlist: WordListMetadata?) : Action { - private val mClientId: String - // The word list to delete. May not be null. - val mWordList: WordListMetadata? - - override fun execute(context: Context?) { - if (null == mWordList) { // This should never happen - Log.e(TAG, "StartDeleteAction with a null word list!") - return - } - DebugLogUtils.l("Trying to delete word list : $mWordList") - val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId) - val values: ContentValues = MetadataDbHelper.getContentValuesByWordListId(db, - mWordList.mId, mWordList.mVersion)!! - if (null == values) { - Log.e(TAG, "Trying to set a non-existing wordlist for removal. Cancelling.") - return - } - val status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN) - if (MetadataDbHelper.STATUS_DISABLED != status) { - Log.e(TAG, "Unexpected status for deleting a word list info : $status") - } - MetadataDbHelper.markEntryAsDeleting(db, mWordList.mId, mWordList.mVersion) - } - - companion object { - val TAG = "DictionaryProvider:" + StartDeleteAction::class.java.simpleName - } - - init { - DebugLogUtils.l("New StartDelete action for client ", clientId, " : ", wordlist) - mClientId = clientId - mWordList = wordlist - } - } - - /** - * An action that validates a word list as deleted. - * - * This will restore the word list as available if it still is, or remove the entry if - * it is not any more. - */ - class FinishDeleteAction(clientId: String, wordlist: WordListMetadata?) : Action { - private val mClientId: String - // The word list to delete. May not be null. - val mWordList: WordListMetadata? - - override fun execute(context: Context?) { - if (null == mWordList) { // This should never happen - Log.e(TAG, "FinishDeleteAction with a null word list!") - return - } - DebugLogUtils.l("Trying to delete word list : $mWordList") - val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId) - val values: ContentValues = MetadataDbHelper.getContentValuesByWordListId(db, - mWordList.mId, mWordList.mVersion)!! - if (null == values) { - Log.e(TAG, "Trying to set a non-existing wordlist for removal. Cancelling.") - return - } - val status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN) - if (MetadataDbHelper.STATUS_DELETING != status) { - Log.e(TAG, "Unexpected status for finish-deleting a word list info : $status") - } - val remoteFilename = values.getAsString(MetadataDbHelper.REMOTE_FILENAME_COLUMN) - // If there isn't a remote filename any more, then we don't know where to get the file -// from any more, so we remove the entry entirely. As a matter of fact, if the file was -// marked DELETING but disappeared from the metadata on the server, it ended up -// this way. - if (TextUtils.isEmpty(remoteFilename)) { - db.delete(MetadataDbHelper.METADATA_TABLE_NAME, - MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND " - + MetadataDbHelper.VERSION_COLUMN + " = ?", arrayOf(mWordList.mId, Integer.toString(mWordList.mVersion))) - } else { - MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion) - } - } - - companion object { - val TAG = "DictionaryProvider:" + FinishDeleteAction::class.java.simpleName - } - - init { - DebugLogUtils.l("New FinishDelete action for client", clientId, " : ", wordlist) - mClientId = clientId - mWordList = wordlist - } - } - - // An action batch consists of an ordered queue of Actions that can execute. - private val mActions: Queue - - fun add(a: Action) { - mActions.add(a) - } - - /** - * Append all the actions of another action batch. - * @param that the upgrade to merge into this one. - */ - fun append(that: ActionBatch) { - for (a in that.mActions) { - add(a) - } - } - - /** - * Execute this batch. - * - * @param context the context for getting resources, databases, system services. - * @param reporter a Reporter to send errors to. - */ - fun execute(context: Context?, reporter: ProblemReporter?) { - DebugLogUtils.l("Executing a batch of actions") - val remainingActions = mActions - while (!remainingActions.isEmpty()) { - val a = remainingActions.poll() - try { - a.execute(context) - } catch (e: Exception) { - reporter?.report(e) - } - } - } - - init { - mActions = LinkedList() - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/BadFormatException.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/BadFormatException.kt deleted file mode 100644 index 1c8263180..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/BadFormatException.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -/** - * Exception thrown when the metadata for the dictionary does not comply to a known format. - */ -class BadFormatException : Exception { - constructor() : super() - constructor(message: String?) : super(message) -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ButtonSwitcher.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ButtonSwitcher.kt deleted file mode 100644 index b819dd937..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ButtonSwitcher.kt +++ /dev/null @@ -1,135 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.view.ViewPropertyAnimator -import android.widget.Button -import android.widget.FrameLayout -import org.dslul.openboard.inputmethod.latin.R - -/** - * A view that handles buttons inside it according to a status. - */ -class ButtonSwitcher : FrameLayout { - // One of the above - private var mStatus = NOT_INITIALIZED - private var mAnimateToStatus = NOT_INITIALIZED - private var mInstallButton: Button? = null - private var mCancelButton: Button? = null - private var mDeleteButton: Button? = null - private var mInterfaceState: DictionaryListInterfaceState? = null - private var mOnClickListener: OnClickListener? = null - - constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) - constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context!!, attrs, defStyle) - - fun reset(interfaceState: DictionaryListInterfaceState?) { - mStatus = NOT_INITIALIZED - mAnimateToStatus = NOT_INITIALIZED - mInterfaceState = interfaceState - } - - override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, - bottom: Int) { - super.onLayout(changed, left, top, right, bottom) - mInstallButton = findViewById(R.id.dict_install_button) as Button - mCancelButton = findViewById(R.id.dict_cancel_button) as Button - mDeleteButton = findViewById(R.id.dict_delete_button) as Button - setInternalOnClickListener(mOnClickListener) - setButtonPositionWithoutAnimation(mStatus) - if (mAnimateToStatus != NOT_INITIALIZED) { // We have been asked to animate before we were ready, so we took a note of it. -// We are now ready: launch the animation. - animateButtonPosition(mStatus, mAnimateToStatus) - mStatus = mAnimateToStatus - mAnimateToStatus = NOT_INITIALIZED - } - } - - private fun getButton(status: Int): Button? { - return when (status) { - STATUS_INSTALL -> mInstallButton - STATUS_CANCEL -> mCancelButton - STATUS_DELETE -> mDeleteButton - else -> null - } - } - - fun setStatusAndUpdateVisuals(status: Int) { - if (mStatus == NOT_INITIALIZED) { - setButtonPositionWithoutAnimation(status) - mStatus = status - } else { - if (null == mInstallButton) { // We may come here before we have been layout. In this case we don't know our -// size yet so we can't start animations so we need to remember what animation to -// start once layout has gone through. - mAnimateToStatus = status - } else { - animateButtonPosition(mStatus, status) - mStatus = status - } - } - } - - private fun setButtonPositionWithoutAnimation(status: Int) { // This may be called by setStatus() before the layout has come yet. - if (null == mInstallButton) return - val width = width - // Set to out of the screen if that's not the currently displayed status - mInstallButton!!.translationX = if (STATUS_INSTALL == status) 0F else width.toFloat() - mCancelButton!!.translationX = if (STATUS_CANCEL == status) 0F else width.toFloat() - mDeleteButton!!.translationX = if (STATUS_DELETE == status) 0F else width.toFloat() - } - - // The helper method for {@link AnimatorListenerAdapter}. - fun animateButtonIfStatusIsEqual(newButton: View, newStatus: Int) { - if (newStatus != mStatus) return - animateButton(newButton, ANIMATION_IN) - } - - private fun animateButtonPosition(oldStatus: Int, newStatus: Int) { - val oldButton: View? = getButton(oldStatus) - val newButton: View? = getButton(newStatus) - if (null != oldButton && null != newButton) { // Transition between two buttons : animate out, then in - animateButton(oldButton, ANIMATION_OUT).setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - animateButtonIfStatusIsEqual(newButton, newStatus) - } - }) - } else oldButton?.let { animateButton(it, ANIMATION_OUT) } - ?: newButton?.let { animateButton(it, ANIMATION_IN) } - } - - fun setInternalOnClickListener(listener: OnClickListener?) { - mOnClickListener = listener - if (null != mInstallButton) { // Already laid out : do it now - mInstallButton!!.setOnClickListener(mOnClickListener) - mCancelButton!!.setOnClickListener(mOnClickListener) - mDeleteButton!!.setOnClickListener(mOnClickListener) - } - } - - private fun animateButton(button: View, direction: Int): ViewPropertyAnimator { - val outerX = width.toFloat() - val innerX = button.x - button.translationX - mInterfaceState!!.removeFromCache(parent as View) - if (ANIMATION_IN == direction) { - button.isClickable = true - return button.animate().translationX(0f) - } - button.isClickable = false - return button.animate().translationX(outerX - innerX) - } - - companion object { - const val NOT_INITIALIZED = -1 - const val STATUS_NO_BUTTON = 0 - const val STATUS_INSTALL = 1 - const val STATUS_CANCEL = 2 - const val STATUS_DELETE = 3 - // Animation directions - const val ANIMATION_IN = 1 - const val ANIMATION_OUT = 2 - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/CommonPreferences.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/CommonPreferences.kt deleted file mode 100644 index 06a393b88..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/CommonPreferences.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.content.Context -import android.content.SharedPreferences - -object CommonPreferences { - private const val COMMON_PREFERENCES_NAME = "LatinImeDictPrefs" - fun getCommonPreferences(context: Context): SharedPreferences { - return context.getSharedPreferences(COMMON_PREFERENCES_NAME, 0) - } - - fun enable(pref: SharedPreferences?, id: String?) { - val editor = pref!!.edit() - editor.putBoolean(id, true) - editor.apply() - } - - fun disable(pref: SharedPreferences?, id: String?) { - val editor = pref!!.edit() - editor.putBoolean(id, false) - editor.apply() - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryListInterfaceState.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryListInterfaceState.kt deleted file mode 100644 index f8244dfd9..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryListInterfaceState.kt +++ /dev/null @@ -1,64 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.view.View -import java.util.* - -/** - * Helper class to maintain the interface state of word list preferences. - * - * This is necessary because the views are created on-demand by calling code. There are many - * situations where views are renewed with little relation with user interaction. For example, - * when scrolling, the view is reused so it doesn't keep its state, which means we need to keep - * it separately. Also whenever the underlying dictionary list undergoes a change (for example, - * update the metadata, or finish downloading) the whole list has to be thrown out and recreated - * in case some dictionaries appeared, disappeared, changed states etc. - */ -class DictionaryListInterfaceState { - internal class State { - var mOpen = false - var mStatus: Int = MetadataDbHelper.Companion.STATUS_UNKNOWN - } - - private val mWordlistToState = HashMap() - private val mViewCache = ArrayList() - fun isOpen(wordlistId: String?): Boolean { - val state = mWordlistToState[wordlistId] ?: return false - return state.mOpen - } - - fun getStatus(wordlistId: String?): Int { - val state = mWordlistToState[wordlistId] ?: return MetadataDbHelper.Companion.STATUS_UNKNOWN - return state.mStatus - } - - fun setOpen(wordlistId: String, status: Int) { - val newState: State - val state = mWordlistToState[wordlistId] - newState = state ?: State() - newState.mOpen = true - newState.mStatus = status - mWordlistToState[wordlistId] = newState - } - - fun closeAll() { - for (state in mWordlistToState.values) { - state.mOpen = false - } - } - - fun findFirstOrphanedView(): View? { - for (v in mViewCache) { - if (null == v.parent) return v - } - return null - } - - fun addToCacheAndReturnView(view: View): View { - mViewCache.add(view) - return view - } - - fun removeFromCache(view: View?) { - mViewCache.remove(view) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryProvider.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryProvider.kt deleted file mode 100644 index 0a9f108c9..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryProvider.kt +++ /dev/null @@ -1,440 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.content.ContentProvider -import android.content.ContentResolver -import android.content.ContentValues -import android.content.UriMatcher -import android.content.res.AssetFileDescriptor -import android.database.AbstractCursor -import android.database.Cursor -import android.database.sqlite.SQLiteDatabase -import android.net.Uri -import android.os.ParcelFileDescriptor -import android.text.TextUtils -import android.util.Log -import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.MarkPreInstalledAction -import org.dslul.openboard.inputmethod.latin.R -import org.dslul.openboard.inputmethod.latin.common.LocaleUtils -import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils -import java.io.FileNotFoundException -import java.util.* - -/** - * Provider for dictionaries. - * - * This class is a ContentProvider exposing all available dictionary data as managed by - * the dictionary pack. - */ -@Suppress("deprecation") -class DictionaryProvider : ContentProvider() { - companion object { - private val TAG = DictionaryProvider::class.java.simpleName - const val DEBUG = false - val CONTENT_URI = Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + DictionaryPackConstants.AUTHORITY) - private const val QUERY_PARAMETER_MAY_PROMPT_USER = "mayPrompt" - private const val QUERY_PARAMETER_TRUE = "true" - private const val QUERY_PARAMETER_DELETE_RESULT = "result" - private const val QUERY_PARAMETER_FAILURE = "failure" - const val QUERY_PARAMETER_PROTOCOL_VERSION = "protocol" - private const val NO_MATCH = 0 - private const val DICTIONARY_V1_WHOLE_LIST = 1 - private const val DICTIONARY_V1_DICT_INFO = 2 - private const val DICTIONARY_V2_METADATA = 3 - private const val DICTIONARY_V2_WHOLE_LIST = 4 - private const val DICTIONARY_V2_DICT_INFO = 5 - private const val DICTIONARY_V2_DATAFILE = 6 - private val sUriMatcherV1 = UriMatcher(NO_MATCH) - private val sUriMatcherV2 = UriMatcher(NO_MATCH) - // MIME types for dictionary and dictionary list, as required by ContentProvider contract. - const val DICT_LIST_MIME_TYPE = "vnd.android.cursor.item/vnd.google.dictionarylist" - const val DICT_DATAFILE_MIME_TYPE = "vnd.android.cursor.item/vnd.google.dictionary" - const val ID_CATEGORY_SEPARATOR = ":" - private fun matchUri(uri: Uri): Int { - var protocolVersion = 1 - val protocolVersionArg = uri.getQueryParameter(QUERY_PARAMETER_PROTOCOL_VERSION) - if ("2" == protocolVersionArg) protocolVersion = 2 - return when (protocolVersion) { - 1 -> sUriMatcherV1.match(uri) - 2 -> sUriMatcherV2.match(uri) - else -> NO_MATCH - } - } - - private fun getClientId(uri: Uri): String? { - var protocolVersion = 1 - val protocolVersionArg = uri.getQueryParameter(QUERY_PARAMETER_PROTOCOL_VERSION) - if ("2" == protocolVersionArg) protocolVersion = 2 - return when (protocolVersion) { - 1 -> null // In protocol 1, the client ID is always null. - 2 -> uri.pathSegments[0] - else -> null - } - } - - init { - sUriMatcherV1.addURI(DictionaryPackConstants.AUTHORITY, "list", DICTIONARY_V1_WHOLE_LIST) - sUriMatcherV1.addURI(DictionaryPackConstants.AUTHORITY, "*", DICTIONARY_V1_DICT_INFO) - sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/metadata", - DICTIONARY_V2_METADATA) - sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/list", DICTIONARY_V2_WHOLE_LIST) - sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/dict/*", - DICTIONARY_V2_DICT_INFO) - sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/datafile/*", - DICTIONARY_V2_DATAFILE) - } - } - - private class WordListInfo(val mId: String, val mLocale: String, val mRawChecksum: String, - val mMatchLevel: Int) - - /** - * A cursor for returning a list of file ids from a List of strings. - * - * This simulates only the necessary methods. It has no error handling to speak of, - * and does not support everything a database does, only a few select necessary methods. - */ - private class ResourcePathCursor(wordLists: Collection) : AbstractCursor() { - // The list of word lists served by this provider that match the client request. - val mWordLists: Array - - override fun getColumnNames(): Array { - return columnNames - } - - override fun getCount(): Int { - return mWordLists.size - } - - override fun getDouble(column: Int): Double { - return 0.0 - } - - override fun getFloat(column: Int): Float { - return 0F - } - - override fun getInt(column: Int): Int { - return 0 - } - - override fun getShort(column: Int): Short { - return 0 - } - - override fun getLong(column: Int): Long { - return 0 - } - - override fun getString(column: Int): String? { - return when (column) { - 0 -> mWordLists[mPos].mId - 1 -> mWordLists[mPos].mLocale - 2 -> mWordLists[mPos].mRawChecksum - else -> null - } - } - - override fun isNull(column: Int): Boolean { - return if (mPos >= mWordLists.size) true else column != 0 - } - - companion object { - // Column names for the cursor returned by this content provider. - private val columnNames = arrayOf(MetadataDbHelper.WORDLISTID_COLUMN, - MetadataDbHelper.LOCALE_COLUMN, MetadataDbHelper.RAW_CHECKSUM_COLUMN) - } - - // Note : the cursor also uses mPos, which is defined in AbstractCursor. - init { // Allocating a 0-size WordListInfo here allows the toArray() method -// to ensure we have a strongly-typed array. It's thrown out. That's -// what the documentation of #toArray says to do in order to get a -// new strongly typed array of the correct size. - mWordLists = wordLists.toTypedArray() - mPos = 0 - } - } - - override fun onCreate(): Boolean { - return true - } - - /** - * Returns the MIME type of the content associated with an Uri - * - * @see android.content.ContentProvider.getType - * @param uri the URI of the content the type of which should be returned. - * @return the MIME type, or null if the URL is not recognized. - */ - override fun getType(uri: Uri): String? { - PrivateLog.log("Asked for type of : $uri") - val match = matchUri(uri) - return when (match) { - NO_MATCH -> null - DICTIONARY_V1_WHOLE_LIST, DICTIONARY_V1_DICT_INFO, DICTIONARY_V2_WHOLE_LIST, DICTIONARY_V2_DICT_INFO -> DICT_LIST_MIME_TYPE - DICTIONARY_V2_DATAFILE -> DICT_DATAFILE_MIME_TYPE - else -> null - } - } - - /** - * Query the provider for dictionary files. - * - * This version dispatches the query according to the protocol version found in the - * ?protocol= query parameter. If absent or not well-formed, it defaults to 1. - * @see android.content.ContentProvider.query - * @param uri a content uri (see sUriMatcherV{1,2} at the top of this file for format) - * @param projection ignored. All columns are always returned. - * @param selection ignored. - * @param selectionArgs ignored. - * @param sortOrder ignored. The results are always returned in no particular order. - * @return a cursor matching the uri, or null if the URI was not recognized. - */ - override fun query(uri: Uri, projection: Array?, selection: String?, - selectionArgs: Array?, sortOrder: String?): Cursor? { - DebugLogUtils.l("Uri =", uri) - PrivateLog.log("Query : $uri") - val clientId = getClientId(uri) - val match = matchUri(uri) - return when (match) { - DICTIONARY_V1_WHOLE_LIST, DICTIONARY_V2_WHOLE_LIST -> { - val c: Cursor = MetadataDbHelper.queryDictionaries(context, clientId) - DebugLogUtils.l("List of dictionaries with count", c.count) - PrivateLog.log("Returned a list of " + c.count + " items") - c - } - DICTIONARY_V2_DICT_INFO -> { - // In protocol version 2, we return null if the client is unknown. Otherwise -// we behave exactly like for protocol 1. - if (!MetadataDbHelper.isClientKnown(context, clientId)) return null - val locale = uri.lastPathSegment - val dictFiles = getDictionaryWordListsForLocale(clientId, locale) - // TODO: pass clientId to the following function - if (null != dictFiles && dictFiles.size > 0) { - PrivateLog.log("Returned " + dictFiles.size + " files") - return ResourcePathCursor(dictFiles) - } - PrivateLog.log("No dictionary files for this URL") - ResourcePathCursor(emptyList()) - } - DICTIONARY_V1_DICT_INFO -> { - val locale = uri.lastPathSegment - val dictFiles = getDictionaryWordListsForLocale(clientId, locale) - if (null != dictFiles && dictFiles.size > 0) { - PrivateLog.log("Returned " + dictFiles.size + " files") - return ResourcePathCursor(dictFiles) - } - PrivateLog.log("No dictionary files for this URL") - ResourcePathCursor(emptyList()) - } - else -> null - } - } - - /** - * Helper method to get the wordlist metadata associated with a wordlist ID. - * - * @param clientId the ID of the client - * @param wordlistId the ID of the wordlist for which to get the metadata. - * @return the metadata for this wordlist ID, or null if none could be found. - */ - private fun getWordlistMetadataForWordlistId(clientId: String?, - wordlistId: String?): ContentValues? { - val context = context - if (TextUtils.isEmpty(wordlistId)) return null - val db: SQLiteDatabase = MetadataDbHelper.getDb(context, clientId) - return MetadataDbHelper.getInstalledOrDeletingWordListContentValuesByWordListId( - db, wordlistId) - } - - /** - * Opens an asset file for an URI. - * - * Called by [android.content.ContentResolver.openAssetFileDescriptor] or - * [android.content.ContentResolver.openInputStream] from a client requesting a - * dictionary. - * @see android.content.ContentProvider.openAssetFile - * @param uri the URI the file is for. - * @param mode the mode to read the file. MUST be "r" for readonly. - * @return the descriptor, or null if the file is not found or if mode is not equals to "r". - */ - override fun openAssetFile(uri: Uri, mode: String): AssetFileDescriptor? { - if (null == mode || "r" != mode) return null - val match = matchUri(uri) - if (DICTIONARY_V1_DICT_INFO != match && DICTIONARY_V2_DATAFILE != match) { // Unsupported URI for openAssetFile - Log.w(TAG, "Unsupported URI for openAssetFile : $uri") - return null - } - val wordlistId = uri.lastPathSegment - val clientId = getClientId(uri) - val wordList = getWordlistMetadataForWordlistId(clientId, wordlistId) ?: return null - try { - val status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN) - if (MetadataDbHelper.STATUS_DELETING == status) { // This will return an empty file (R.raw.empty points at an empty dictionary) -// This is how we "delete" the files. It allows Android Keyboard to fake deleting -// a default dictionary - which is actually in its assets and can't be really -// deleted. - return context!!.resources.openRawResourceFd( - R.raw.empty) - } - val localFilename = wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN) - val f = context!!.getFileStreamPath(localFilename) - val pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY) - return AssetFileDescriptor(pfd, 0, pfd.statSize) - } catch (e: FileNotFoundException) { // No file : fall through and return null - } - return null - } - - /** - * Reads the metadata and returns the collection of dictionaries for a given locale. - * - * Word list IDs are expected to be in the form category:manual_id. This method - * will select only one word list for each category: the one with the most specific - * locale matching the locale specified in the URI. The manual id serves only to - * distinguish a word list from another for the purpose of updating, and is arbitrary - * but may not contain a colon. - * - * @param clientId the ID of the client requesting the list - * @param locale the locale for which we want the list, as a String - * @return a collection of ids. It is guaranteed to be non-null, but may be empty. - */ - private fun getDictionaryWordListsForLocale(clientId: String?, - locale: String?): Collection { - val context = context - val results: Cursor = MetadataDbHelper.queryInstalledOrDeletingOrAvailableDictionaryMetadata(context, - clientId) - return try { - val dicts = HashMap() - val idIndex = results.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN) - val localeIndex = results.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN) - val localFileNameIndex = results.getColumnIndex(MetadataDbHelper.LOCAL_FILENAME_COLUMN) - val rawChecksumIndex = results.getColumnIndex(MetadataDbHelper.RAW_CHECKSUM_COLUMN) - val statusIndex = results.getColumnIndex(MetadataDbHelper.STATUS_COLUMN) - if (results.moveToFirst()) { - do { - val wordListId = results.getString(idIndex) - if (TextUtils.isEmpty(wordListId)) continue - val wordListIdArray = TextUtils.split(wordListId, ID_CATEGORY_SEPARATOR) - val wordListCategory: String - // This is at the category:manual_id format. - wordListCategory = wordListIdArray[0] - val wordListLocale = results.getString(localeIndex) - val wordListLocalFilename = results.getString(localFileNameIndex) - val wordListRawChecksum = results.getString(rawChecksumIndex) - val wordListStatus = results.getInt(statusIndex) - // Test the requested locale against this wordlist locale. The requested locale -// has to either match exactly or be more specific than the dictionary - a -// dictionary for "en" would match both a request for "en" or for "en_US", but a -// dictionary for "en_GB" would not match a request for "en_US". Thus if all -// three of "en" "en_US" and "en_GB" dictionaries are installed, a request for -// "en_US" would match "en" and "en_US", and a request for "en" only would only -// match the generic "en" dictionary. For more details, see the documentation -// for LocaleUtils#getMatchLevel. - val matchLevel = LocaleUtils.getMatchLevel(wordListLocale, locale) - if (!LocaleUtils.isMatch(matchLevel)) { // The locale of this wordlist does not match the required locale. -// Skip this wordlist and go to the next. - continue - } - if (MetadataDbHelper.STATUS_INSTALLED == wordListStatus) { // If the file does not exist, it has been deleted and the IME should -// already have it. Do not return it. However, this only applies if the -// word list is INSTALLED, for if it is DELETING we should return it always -// so that Android Keyboard can perform the actual deletion. - val f = getContext()!!.getFileStreamPath(wordListLocalFilename) - if (!f.isFile) { - continue - } - } - val currentBestMatch = dicts[wordListCategory] - if (null == currentBestMatch - || currentBestMatch.mMatchLevel < matchLevel) { - dicts[wordListCategory] = WordListInfo(wordListId, wordListLocale, - wordListRawChecksum, matchLevel) - } - } while (results.moveToNext()) - } - Collections.unmodifiableCollection(dicts.values) - } finally { - results.close() - } - } - - /** - * Deletes the file pointed by Uri, as returned by openAssetFile. - * - * @param uri the URI the file is for. - * @param selection ignored - * @param selectionArgs ignored - * @return the number of files deleted (0 or 1 in the current implementation) - * @see android.content.ContentProvider.delete - */ - @Throws(UnsupportedOperationException::class) - override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { - val match = matchUri(uri) - if (DICTIONARY_V1_DICT_INFO == match || DICTIONARY_V2_DATAFILE == match) { - return deleteDataFile(uri) - } - return if (DICTIONARY_V2_METADATA == match) { - if (MetadataDbHelper.deleteClient(context, getClientId(uri))) { - 1 - } else 0 - } else 0 - // Unsupported URI for delete - } - - private fun deleteDataFile(uri: Uri): Int { - val wordlistId = uri.lastPathSegment - val clientId = getClientId(uri) - val wordList = getWordlistMetadataForWordlistId(clientId, wordlistId) ?: return 0 - val status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN) - val version = wordList.getAsInteger(MetadataDbHelper.VERSION_COLUMN) - return 0 - } - - /** - * Insert data into the provider. May be either a metadata source URL or some dictionary info. - * - * @param uri the designated content URI. See sUriMatcherV{1,2} for available URIs. - * @param values the values to insert for this content uri - * @return the URI for the newly inserted item. May be null if arguments don't allow for insert - */ - @Throws(UnsupportedOperationException::class) - override fun insert(uri: Uri, values: ContentValues?): Uri? { - if (null == values) return null // Should never happen but let's be safe - PrivateLog.log("Insert, uri = $uri") - val clientId = getClientId(uri) - when (matchUri(uri)) { - DICTIONARY_V2_METADATA -> // The values should contain a valid client ID and a valid URI for the metadata. -// The client ID may not be null, nor may it be empty because the empty client ID -// is reserved for internal use. -// The metadata URI may not be null, but it may be empty if the client does not -// want the dictionary pack to update the metadata automatically. - MetadataDbHelper.updateClientInfo(context, clientId, values) - DICTIONARY_V2_DICT_INFO -> try { - val newDictionaryMetadata: WordListMetadata = WordListMetadata.createFromContentValues( - MetadataDbHelper.completeWithDefaultValues(values)) - MarkPreInstalledAction(clientId, newDictionaryMetadata) - .execute(context) - } catch (e: BadFormatException) { - Log.w(TAG, "Not enough information to insert this dictionary $values", e) - } - DICTIONARY_V1_WHOLE_LIST, DICTIONARY_V1_DICT_INFO -> { - PrivateLog.log("Attempt to insert : $uri") - throw UnsupportedOperationException( - "Insertion in the dictionary is not supported in this version") - } - } - return uri - } - - /** - * Updating data is not supported, and will throw an exception. - * @see android.content.ContentProvider.update - * @see android.content.ContentProvider.insert - */ - @Throws(UnsupportedOperationException::class) - override fun update(uri: Uri, values: ContentValues?, selection: String?, - selectionArgs: Array?): Int { - PrivateLog.log("Attempt to update : $uri") - throw UnsupportedOperationException("Updating dictionary words is not supported") - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsFragment.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsFragment.kt deleted file mode 100644 index 3027bacb1..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsFragment.kt +++ /dev/null @@ -1,224 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.app.Activity -import android.content.ContentResolver -import android.content.Intent -import android.content.IntentFilter -import android.net.Uri -import android.os.Bundle -import android.preference.Preference -import android.preference.PreferenceFragment -import android.preference.PreferenceGroup -import android.util.Log -import android.view.* -import org.dslul.openboard.inputmethod.latin.R -import org.dslul.openboard.inputmethod.latin.common.LocaleUtils -import java.util.* - -/** - * Preference screen. - */ -// keep the fragment for now, as it may contain something useful on dictionary management, but -// suppress warnings -// todo: check this fragment, take what could be useful, and remove the fragment -// same for WordListPreference and WordListMetadata -@Suppress("deprecation") -class DictionarySettingsFragment -/** - * Empty constructor for fragment generation. - */ - : PreferenceFragment() { - private var mLoadingView: View? = null - private var mClientId: String? = null - //private ConnectivityManager mConnectivityManager; - private val mUpdateNowMenu: MenuItem? = null - private var mChangedSettings = false - private val mDictionaryListInterfaceState = DictionaryListInterfaceState() - // never null - private var mCurrentPreferenceMap = TreeMap() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - val v = inflater.inflate(R.layout.loading_page, container, true) - mLoadingView = v.findViewById(R.id.loading_container) - return super.onCreateView(inflater, container, savedInstanceState) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - val activity = activity - mClientId = activity.intent.getStringExtra(DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT) - /*mConnectivityManager = - (ConnectivityManager)activity.getSystemService(Context.CONNECTIVITY_SERVICE);*/addPreferencesFromResource(R.xml.dictionary_settings) - refreshInterface() - setHasOptionsMenu(true) - } - - override fun onResume() { - super.onResume() - mChangedSettings = false - val activity = activity - val filter = IntentFilter() - object : Thread("onResume") { - override fun run() { - if (!MetadataDbHelper.Companion.isClientKnown(activity, mClientId)) { - Log.i(TAG, "Unknown dictionary pack client: " + mClientId - + ". Requesting info.") - val unknownClientBroadcast = Intent(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT) - unknownClientBroadcast.putExtra( - DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA, mClientId) - activity.sendBroadcast(unknownClientBroadcast) - } - } - }.start() - } - - override fun onPause() { - super.onPause() - val activity = activity - if (mChangedSettings) { - val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION) - activity.sendBroadcast(newDictBroadcast) - mChangedSettings = false - } - } - - private fun findWordListPreference(id: String): WordListPreference? { - val prefScreen: PreferenceGroup? = preferenceScreen - if (null == prefScreen) { - Log.e(TAG, "Could not find the preference group") - return null - } - for (i in prefScreen.preferenceCount - 1 downTo 0) { - val pref = prefScreen.getPreference(i) - if (pref is WordListPreference) { - val wlPref = pref - if (id == wlPref.mWordlistId) { - return wlPref - } - } - } - Log.e(TAG, "Could not find the preference for a word list id $id") - return null - } - - fun refreshInterface() { - val activity = activity ?: return - val prefScreen: PreferenceGroup = preferenceScreen - val prefList = createInstalledDictSettingsCollection(mClientId) - activity.runOnUiThread { - // TODO: display this somewhere -// if (0 != lastUpdate) mUpdateNowPreference.setSummary(updateNowSummary); - removeAnyDictSettings(prefScreen) - var i = 0 - for (preference in prefList) { - preference.order = i++ - prefScreen.addPreference(preference) - } - } - } - - /** - * Creates a WordListPreference list to be added to the screen. - * - * This method only creates the preferences but does not add them. - * Thus, it can be called on another thread. - * - * @param clientId the id of the client for which we want to display the dictionary list - * @return A collection of preferences ready to add to the interface. - */ - private fun createInstalledDictSettingsCollection( - clientId: String?): Collection { // This will directly contact the DictionaryProvider and request the list exactly like -// any regular client would do. -// Considering the respective value of the respective constants used here for each path, -// segment, the url generated by this is of the form (assuming "clientId" as a clientId) -// content://org.dslul.openboard.inputmethod.latin.dictionarypack/clientId/list?procotol=2 - val contentUri = Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) - .authority(getString(R.string.authority)) - .appendPath(clientId) - .appendPath(DICT_LIST_ID) // Need to use version 2 to get this client's list - .appendQueryParameter(DictionaryProvider.Companion.QUERY_PARAMETER_PROTOCOL_VERSION, "2") - .build() - val activity = activity - val cursor = activity?.contentResolver?.query(contentUri, null, null, null, null) - if (null == cursor) { - val result = ArrayList() - result.add(createErrorMessage(activity, R.string.cannot_connect_to_dict_service)) - return result - } - return try { - if (!cursor.moveToFirst()) { - val result = ArrayList() - result.add(createErrorMessage(activity, R.string.no_dictionaries_available)) - return result - } - val systemLocaleString = Locale.getDefault().toString() - val prefMap = TreeMap() - val idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN) - val versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN) - val localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN) - val descriptionIndex = cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN) - val statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN) - val filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN) - do { - val wordlistId = cursor.getString(idIndex) - val version = cursor.getInt(versionIndex) - val localeString = cursor.getString(localeIndex) - val locale = Locale(localeString) - val description = cursor.getString(descriptionIndex) - val status = cursor.getInt(statusIndex) - val matchLevel = LocaleUtils.getMatchLevel(systemLocaleString, localeString) - val matchLevelString = LocaleUtils.getMatchLevelSortedString(matchLevel) - val filesize = cursor.getInt(filesizeIndex) - // The key is sorted in lexicographic order, according to the match level, then -// the description. - val key = "$matchLevelString.$description.$wordlistId" - val existingPref = prefMap[key] - if (null == existingPref || existingPref.hasPriorityOver(status)) { - val oldPreference = mCurrentPreferenceMap[key] - val pref: WordListPreference - pref = if (null != oldPreference && oldPreference.mVersion == version && oldPreference.hasStatus(status) - && oldPreference.mLocale == locale) { // If the old preference has all the new attributes, reuse it. Ideally, -// we should reuse the old pref even if its status is different and call -// setStatus here, but setStatus calls Preference#setSummary() which -// needs to be done on the UI thread and we're not on the UI thread -// here. We could do all this work on the UI thread, but in this case -// it's probably lighter to stay on a background thread and throw this -// old preference out. - oldPreference - } else { // Otherwise, discard it and create a new one instead. -// TODO: when the status is different from the old one, we need to -// animate the old one out before animating the new one in. - WordListPreference(activity, mDictionaryListInterfaceState, - mClientId, wordlistId, version, locale, description, status, - filesize) - } - prefMap[key] = pref - } - } while (cursor.moveToNext()) - mCurrentPreferenceMap = prefMap - prefMap.values - } finally { - cursor.close() - } - } - - companion object { - private val TAG = DictionarySettingsFragment::class.java.simpleName - private const val DICT_LIST_ID = "list" - const val DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT = "clientId" -// private const val MENU_UPDATE_NOW = Menu.FIRST - private fun createErrorMessage(activity: Activity?, messageResource: Int): Preference { - val message = Preference(activity) - message.setTitle(messageResource) - message.isEnabled = false - return message - } - - fun removeAnyDictSettings(prefGroup: PreferenceGroup) { - for (i in prefGroup.preferenceCount - 1 downTo 0) { - prefGroup.removePreference(prefGroup.getPreference(i)) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/LogProblemReporter.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/LogProblemReporter.kt deleted file mode 100644 index 6622e1678..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/LogProblemReporter.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.util.Log - -/** - * A very simple problem reporter. - */ -internal class LogProblemReporter(private val TAG: String) : ProblemReporter { - override fun report(e: Exception?) { - Log.e(TAG, "Reporting problem", e) - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataDbHelper.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataDbHelper.kt deleted file mode 100644 index ec211c2cc..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataDbHelper.kt +++ /dev/null @@ -1,935 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.content.ContentValues -import android.content.Context -import android.database.Cursor -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteException -import android.database.sqlite.SQLiteOpenHelper -import android.text.TextUtils -import android.util.Log -import org.dslul.openboard.inputmethod.latin.R -import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils -import java.io.File -import java.util.* - -/** - * Various helper functions for the state database - */ -class MetadataDbHelper private constructor(private val mContext: Context?, private val mClientId: String) : SQLiteOpenHelper(mContext, - METADATA_DATABASE_NAME_STEM + if (TextUtils.isEmpty(mClientId)) "" else ".$mClientId", - null, CURRENT_METADATA_DATABASE_VERSION) { - private fun createClientTable(db: SQLiteDatabase) { // The clients table only exists in the primary db, the one that has an empty client id - if (!TextUtils.isEmpty(mClientId)) return - db.execSQL(METADATA_CREATE_CLIENT_TABLE) - val defaultMetadataUri = mContext!!.getString(R.string.default_metadata_uri) - if (!TextUtils.isEmpty(defaultMetadataUri)) { - val defaultMetadataValues = ContentValues() - defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, "") - defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri) - db.insert(CLIENT_TABLE_NAME, null, defaultMetadataValues) - } - } - - /** - * Create the table and populate it with the resources found inside the apk. - * - * @see SQLiteOpenHelper.onCreate - * @param db the database to create and populate. - */ - override fun onCreate(db: SQLiteDatabase) { - db.execSQL(METADATA_TABLE_CREATE) - createClientTable(db) - } - - /** - * Upgrade the database. Upgrade from version 3 is supported. - * Version 3 has a DB named METADATA_DATABASE_NAME_STEM containing a table METADATA_TABLE_NAME. - * Version 6 and above has a DB named METADATA_DATABASE_NAME_STEM containing a - * table CLIENT_TABLE_NAME, and for each client a table called METADATA_TABLE_STEM + "." + the - * name of the client and contains a table METADATA_TABLE_NAME. - * For schemas, see the above create statements. The schemas have never changed so far. - * - * This method is called by the framework. See [SQLiteOpenHelper.onUpgrade] - * @param db The database we are upgrading - * @param oldVersion The old database version (the one on the disk) - * @param newVersion The new database version as supplied to the constructor of SQLiteOpenHelper - */ - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - if (METADATA_DATABASE_INITIAL_VERSION == oldVersion && METADATA_DATABASE_VERSION_WITH_CLIENTID <= newVersion && CURRENT_METADATA_DATABASE_VERSION >= newVersion) { // Upgrade from version METADATA_DATABASE_INITIAL_VERSION to version -// METADATA_DATABASE_VERSION_WITH_CLIENT_ID -// Only the default database should contain the client table, so we test for mClientId. - if (TextUtils.isEmpty(mClientId)) { // Anyway in version 3 only the default table existed so the emptiness -// test should always be true, but better check to be sure. - createClientTable(db) - } - } else if (METADATA_DATABASE_VERSION_WITH_CLIENTID < newVersion - && CURRENT_METADATA_DATABASE_VERSION >= newVersion) { // Here we drop the client table, so that all clients send us their information again. -// The client table contains the URL to hit to update the available dictionaries list, -// but the info about the dictionaries themselves is stored in the table called -// METADATA_TABLE_NAME and we want to keep it, so we only drop the client table. - db.execSQL("DROP TABLE IF EXISTS $CLIENT_TABLE_NAME") - // Only the default database should contain the client table, so we test for mClientId. - if (TextUtils.isEmpty(mClientId)) { - createClientTable(db) - } - } else { // If we're not in the above case, either we are upgrading from an earlier versionCode -// and we should wipe the database, or we are handling a version we never heard about -// (can only be a bug) so it's safer to wipe the database. - db.execSQL("DROP TABLE IF EXISTS $METADATA_TABLE_NAME") - db.execSQL("DROP TABLE IF EXISTS $CLIENT_TABLE_NAME") - onCreate(db) - } - // A rawChecksum column that did not exist in the previous versions was added that -// corresponds to the md5 checksum of the file after decompression/decryption. This is to -// strengthen the system against corrupted dictionary files. -// The most secure way to upgrade a database is to just test for the column presence, and -// add it if it's not there. - addRawChecksumColumnUnlessPresent(db) - // A retry count column that did not exist in the previous versions was added that -// corresponds to the number of download & installation attempts that have been made -// in order to strengthen the system recovery from corrupted dictionary files. -// The most secure way to upgrade a database is to just test for the column presence, and -// add it if it's not there. - addRetryCountColumnUnlessPresent(db) - } - - /** - * Downgrade the database. This drops and recreates the table in all cases. - */ - override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { // No matter what the numerical values of oldVersion and newVersion are, we know this -// is a downgrade (newVersion < oldVersion). There is no way to know what the future -// databases will look like, but we know it's extremely likely that it's okay to just -// drop the tables and start from scratch. Hence, we ignore the versions and just wipe -// everything we want to use. - if (oldVersion <= newVersion) { - Log.e(TAG, "onDowngrade database but new version is higher? " + oldVersion + " <= " - + newVersion) - } - db.execSQL("DROP TABLE IF EXISTS $METADATA_TABLE_NAME") - db.execSQL("DROP TABLE IF EXISTS $CLIENT_TABLE_NAME") - onCreate(db) - } - - companion object { - private val TAG = MetadataDbHelper::class.java.simpleName - // This was the initial release version of the database. It should never be -// changed going forward. - private const val METADATA_DATABASE_INITIAL_VERSION = 3 - // This is the first released version of the database that implements CLIENTID. It is -// used to identify the versions for upgrades. This should never change going forward. - private const val METADATA_DATABASE_VERSION_WITH_CLIENTID = 6 - // The current database version. -// This MUST be increased every time the dictionary pack metadata URL changes. - private const val CURRENT_METADATA_DATABASE_VERSION = 16 - private const val NOT_A_DOWNLOAD_ID: Long = -1 - // The number of retries allowed when attempting to download a broken dictionary. - const val DICTIONARY_RETRY_THRESHOLD = 2 - const val METADATA_TABLE_NAME = "pendingUpdates" - const val CLIENT_TABLE_NAME = "clients" - const val PENDINGID_COLUMN = "pendingid" // Download Manager ID - const val TYPE_COLUMN = "type" - const val STATUS_COLUMN = "status" - const val LOCALE_COLUMN = "locale" - const val WORDLISTID_COLUMN = "id" - const val DESCRIPTION_COLUMN = "description" - const val LOCAL_FILENAME_COLUMN = "filename" - const val REMOTE_FILENAME_COLUMN = "url" - const val DATE_COLUMN = "date" - const val CHECKSUM_COLUMN = "checksum" - const val FILESIZE_COLUMN = "filesize" - const val VERSION_COLUMN = "version" - const val FORMATVERSION_COLUMN = "formatversion" - const val FLAGS_COLUMN = "flags" - const val RAW_CHECKSUM_COLUMN = "rawChecksum" - const val RETRY_COUNT_COLUMN = "remainingRetries" - const val COLUMN_COUNT = 15 - private const val CLIENT_CLIENT_ID_COLUMN = "clientid" - private const val CLIENT_METADATA_URI_COLUMN = "uri" - private const val CLIENT_METADATA_ADDITIONAL_ID_COLUMN = "additionalid" - private const val CLIENT_LAST_UPDATE_DATE_COLUMN = "lastupdate" - private const val CLIENT_PENDINGID_COLUMN = "pendingid" // Download Manager ID - const val METADATA_DATABASE_NAME_STEM = "pendingUpdates" - const val METADATA_UPDATE_DESCRIPTION = "metadata" - const val DICTIONARIES_ASSETS_PATH = "dictionaries" - // Statuses, for storing in the STATUS_COLUMN -// IMPORTANT: The following are used as index arrays in ../WordListPreference -// Do not change their values without updating the matched code. -// Unknown status: this should never happen. - const val STATUS_UNKNOWN = 0 - // Available: this word list is available, but it is not downloaded (not downloading), because -// it is set not to be used. - const val STATUS_AVAILABLE = 1 - // Downloading: this word list is being downloaded. - const val STATUS_DOWNLOADING = 2 - // Installed: this word list is installed and usable. - const val STATUS_INSTALLED = 3 - // Disabled: this word list is installed, but has been disabled by the user. - const val STATUS_DISABLED = 4 - // Deleting: the user marked this word list to be deleted, but it has not been yet because -// Latin IME is not up yet. - const val STATUS_DELETING = 5 - // Retry: dictionary got corrupted, so an attempt must be done to download & install it again. - const val STATUS_RETRYING = 6 - // Types, for storing in the TYPE_COLUMN -// This is metadata about what is available. - const val TYPE_METADATA = 1 - // This is a bulk file. It should replace older files. - const val TYPE_BULK = 2 - // This is an incremental update, expected to be small, and meaningless on its own. - const val TYPE_UPDATE = 3 - private const val METADATA_TABLE_CREATE = ("CREATE TABLE " + METADATA_TABLE_NAME + " (" - + PENDINGID_COLUMN + " INTEGER, " - + TYPE_COLUMN + " INTEGER, " - + STATUS_COLUMN + " INTEGER, " - + WORDLISTID_COLUMN + " TEXT, " - + LOCALE_COLUMN + " TEXT, " - + DESCRIPTION_COLUMN + " TEXT, " - + LOCAL_FILENAME_COLUMN + " TEXT, " - + REMOTE_FILENAME_COLUMN + " TEXT, " - + DATE_COLUMN + " INTEGER, " - + CHECKSUM_COLUMN + " TEXT, " - + FILESIZE_COLUMN + " INTEGER, " - + VERSION_COLUMN + " INTEGER," - + FORMATVERSION_COLUMN + " INTEGER, " - + FLAGS_COLUMN + " INTEGER, " - + RAW_CHECKSUM_COLUMN + " TEXT," - + RETRY_COUNT_COLUMN + " INTEGER, " - + "PRIMARY KEY (" + WORDLISTID_COLUMN + "," + VERSION_COLUMN + "));") - private const val METADATA_CREATE_CLIENT_TABLE = ("CREATE TABLE IF NOT EXISTS " + CLIENT_TABLE_NAME + " (" - + CLIENT_CLIENT_ID_COLUMN + " TEXT, " - + CLIENT_METADATA_URI_COLUMN + " TEXT, " - + CLIENT_METADATA_ADDITIONAL_ID_COLUMN + " TEXT, " - + CLIENT_LAST_UPDATE_DATE_COLUMN + " INTEGER NOT NULL DEFAULT 0, " - + CLIENT_PENDINGID_COLUMN + " INTEGER, " - + FLAGS_COLUMN + " INTEGER, " - + "PRIMARY KEY (" + CLIENT_CLIENT_ID_COLUMN + "));") - // List of all metadata table columns. - val METADATA_TABLE_COLUMNS = arrayOf(PENDINGID_COLUMN, TYPE_COLUMN, - STATUS_COLUMN, WORDLISTID_COLUMN, LOCALE_COLUMN, DESCRIPTION_COLUMN, - LOCAL_FILENAME_COLUMN, REMOTE_FILENAME_COLUMN, DATE_COLUMN, CHECKSUM_COLUMN, - FILESIZE_COLUMN, VERSION_COLUMN, FORMATVERSION_COLUMN, FLAGS_COLUMN, - RAW_CHECKSUM_COLUMN, RETRY_COUNT_COLUMN) - // List of all client table columns. - val CLIENT_TABLE_COLUMNS = arrayOf(CLIENT_CLIENT_ID_COLUMN, - CLIENT_METADATA_URI_COLUMN, CLIENT_PENDINGID_COLUMN, FLAGS_COLUMN) - // List of public columns returned to clients. Everything that is not in this list is -// private and implementation-dependent. - val DICTIONARIES_LIST_PUBLIC_COLUMNS = arrayOf(STATUS_COLUMN, WORDLISTID_COLUMN, - LOCALE_COLUMN, DESCRIPTION_COLUMN, DATE_COLUMN, FILESIZE_COLUMN, VERSION_COLUMN) - // This class exhibits a singleton-like behavior by client ID, so it is getInstance'd -// and has a private c'tor. - private var sInstanceMap: TreeMap? = null - - @Synchronized - fun getInstance(context: Context?, - clientIdOrNull: String?): MetadataDbHelper { // As a backward compatibility feature, null can be passed here to retrieve the "default" -// database. Before multi-client support, the dictionary packed used only one database -// and would not be able to handle several dictionary sets. Passing null here retrieves -// this legacy database. New clients should make sure to always pass a client ID so as -// to avoid conflicts. - val clientId = clientIdOrNull ?: "" - if (null == sInstanceMap) sInstanceMap = TreeMap() - var helper = sInstanceMap!![clientId] - if (null == helper) { - helper = MetadataDbHelper(context, clientId) - sInstanceMap!![clientId] = helper - } - return helper - } - - /** - * Get the database itself. This always returns the same object for any client ID. If the - * client ID is null, a default database is returned for backward compatibility. Don't - * pass null for new calls. - * - * @param context the context to create the database from. This is ignored after the first call. - * @param clientId the client id to retrieve the database of. null for default (deprecated) - * @return the database. - */ - fun getDb(context: Context?, clientId: String?): SQLiteDatabase { - return getInstance(context, clientId).writableDatabase - } - - private fun addRawChecksumColumnUnlessPresent(db: SQLiteDatabase) { - try { - db.execSQL("SELECT " + RAW_CHECKSUM_COLUMN + " FROM " - + METADATA_TABLE_NAME + " LIMIT 0;") - } catch (e: SQLiteException) { - Log.i(TAG, "No $RAW_CHECKSUM_COLUMN column : creating it") - db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN " - + RAW_CHECKSUM_COLUMN + " TEXT;") - } - } - - private fun addRetryCountColumnUnlessPresent(db: SQLiteDatabase) { - try { - db.execSQL("SELECT " + RETRY_COUNT_COLUMN + " FROM " - + METADATA_TABLE_NAME + " LIMIT 0;") - } catch (e: SQLiteException) { - Log.i(TAG, "No $RETRY_COUNT_COLUMN column : creating it") - db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN " - + RETRY_COUNT_COLUMN + " INTEGER DEFAULT " + DICTIONARY_RETRY_THRESHOLD + ";") - } - } - - /** - * Given a client ID, returns whether this client exists. - * - * @param context a context to open the database - * @param clientId the client ID to check - * @return true if the client is known, false otherwise - */ - fun isClientKnown(context: Context?, clientId: String?): Boolean { // If the client is known, they'll have a non-null metadata URI. An empty string is -// allowed as a metadata URI, if the client doesn't want any updates to happen. - return null != getMetadataUriAsString(context, clientId) - } - - /** - * Returns the metadata URI as a string. - * - * If the client is not known, this will return null. If it is known, it will return - * the URI as a string. Note that the empty string is a valid value. - * - * @param context a context instance to open the database on - * @param clientId the ID of the client we want the metadata URI of - * @return the string representation of the URI - */ - fun getMetadataUriAsString(context: Context?, clientId: String?): String? { - val defaultDb = getDb(context, null) - val cursor = defaultDb.query(CLIENT_TABLE_NAME, arrayOf(CLIENT_METADATA_URI_COLUMN), - "$CLIENT_CLIENT_ID_COLUMN = ?", arrayOf(clientId), - null, null, null, null) - return try { - if (!cursor.moveToFirst()) null else MetadataUriGetter.getUri(context, cursor.getString(0)) - } finally { - cursor.close() - } - } - - /** - * Update the last metadata update time for all clients using a particular URI. - * - * This method searches for all clients using a particular URI and updates the last - * update time for this client. - * The current time is used as the latest update time. This saved date will be what - * is returned henceforth by [.getLastUpdateDateForClient], - * until this method is called again. - * - * @param context a context instance to open the database on - * @param uri the metadata URI we just downloaded - */ - fun saveLastUpdateTimeOfUri(context: Context?, uri: String) { - PrivateLog.log("Save last update time of URI : " + uri + " " + System.currentTimeMillis()) - val values = ContentValues() - values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis()) - val defaultDb = getDb(context, null) - val cursor = queryClientIds(context) - try { - if (!cursor.moveToFirst()) return - do { - val clientId = cursor.getString(0) - val metadataUri = getMetadataUriAsString(context, clientId) - if (metadataUri == uri) { - defaultDb.update(CLIENT_TABLE_NAME, values, - "$CLIENT_CLIENT_ID_COLUMN = ?", arrayOf(clientId)) - } - } while (cursor.moveToNext()) - } finally { - cursor.close() - } - } - - /** - * Retrieves the last date at which we updated the metadata for this client. - * - * The returned date is in milliseconds from the EPOCH; this is the same unit as - * returned by [System.currentTimeMillis]. - * - * @param context a context instance to open the database on - * @param clientId the client ID to get the latest update date of - * @return the last date at which this client was updated, as a long. - */ - fun getLastUpdateDateForClient(context: Context?, clientId: String?): Long { - val defaultDb = getDb(context, null) - val cursor = defaultDb.query(CLIENT_TABLE_NAME, arrayOf(CLIENT_LAST_UPDATE_DATE_COLUMN), - "$CLIENT_CLIENT_ID_COLUMN = ?", arrayOf(clientId ?: ""), - null, null, null, null) - return try { - if (!cursor.moveToFirst()) 0 else cursor.getLong(0) - // Only one column, return it - } finally { - cursor.close() - } - } - - fun getOldestUpdateTime(context: Context?): Long { - val defaultDb = getDb(context, null) - val cursor = defaultDb.query(CLIENT_TABLE_NAME, arrayOf(CLIENT_LAST_UPDATE_DATE_COLUMN), - null, null, null, null, null) - return try { - if (!cursor.moveToFirst()) return 0 - val columnIndex = 0 // Only one column queried - // Initialize the earliestTime to the largest possible value. - var earliestTime = Long.MAX_VALUE // Almost 300 million years in the future - do { - val thisTime = cursor.getLong(columnIndex) - earliestTime = Math.min(thisTime, earliestTime) - } while (cursor.moveToNext()) - earliestTime - } finally { - cursor.close() - } - } - - /** - * Helper method to make content values to write into the database. - * @return content values with all the arguments put with the right column names. - */ - fun makeContentValues(pendingId: Int, type: Int, - status: Int, wordlistId: String?, locale: String?, - description: String?, filename: String?, url: String?, date: Long, - rawChecksum: String?, checksum: String?, retryCount: Int, - filesize: Long, version: Int, formatVersion: Int): ContentValues { - val result = ContentValues(COLUMN_COUNT) - result.put(PENDINGID_COLUMN, pendingId) - result.put(TYPE_COLUMN, type) - result.put(WORDLISTID_COLUMN, wordlistId) - result.put(STATUS_COLUMN, status) - result.put(LOCALE_COLUMN, locale) - result.put(DESCRIPTION_COLUMN, description) - result.put(LOCAL_FILENAME_COLUMN, filename) - result.put(REMOTE_FILENAME_COLUMN, url) - result.put(DATE_COLUMN, date) - result.put(RAW_CHECKSUM_COLUMN, rawChecksum) - result.put(RETRY_COUNT_COLUMN, retryCount) - result.put(CHECKSUM_COLUMN, checksum) - result.put(FILESIZE_COLUMN, filesize) - result.put(VERSION_COLUMN, version) - result.put(FORMATVERSION_COLUMN, formatVersion) - result.put(FLAGS_COLUMN, 0) - return result - } - - /** - * Helper method to fill in an incomplete ContentValues with default values. - * A wordlist ID and a locale are required, otherwise BadFormatException is thrown. - * @return the same object that was passed in, completed with default values. - */ - @Throws(BadFormatException::class) - fun completeWithDefaultValues(result: ContentValues): ContentValues { - if (null == result[WORDLISTID_COLUMN] || null == result[LOCALE_COLUMN]) { - throw BadFormatException() - } - // 0 for the pending id, because there is none - if (null == result[PENDINGID_COLUMN]) result.put(PENDINGID_COLUMN, 0) - // This is a binary blob of a dictionary - if (null == result[TYPE_COLUMN]) result.put(TYPE_COLUMN, TYPE_BULK) - // This word list is unknown, but it's present, else we wouldn't be here, so INSTALLED - if (null == result[STATUS_COLUMN]) result.put(STATUS_COLUMN, STATUS_INSTALLED) - // No description unless specified, because we can't guess it - if (null == result[DESCRIPTION_COLUMN]) result.put(DESCRIPTION_COLUMN, "") - // File name - this is an asset, so it works as an already deleted file. -// hence, we need to supply a non-existent file name. Anything will -// do as long as it returns false when tested with File#exist(), and -// the empty string does not, so it's set to "_". - if (null == result[LOCAL_FILENAME_COLUMN]) result.put(LOCAL_FILENAME_COLUMN, "_") - // No remote file name : this can't be downloaded. Unless specified. - if (null == result[REMOTE_FILENAME_COLUMN]) result.put(REMOTE_FILENAME_COLUMN, "") - // 0 for the update date : 1970/1/1. Unless specified. - if (null == result[DATE_COLUMN]) result.put(DATE_COLUMN, 0) - // Raw checksum unknown unless specified - if (null == result[RAW_CHECKSUM_COLUMN]) result.put(RAW_CHECKSUM_COLUMN, "") - // Retry column 0 unless specified - if (null == result[RETRY_COUNT_COLUMN]) result.put(RETRY_COUNT_COLUMN, - DICTIONARY_RETRY_THRESHOLD) - // Checksum unknown unless specified - if (null == result[CHECKSUM_COLUMN]) result.put(CHECKSUM_COLUMN, "") - // No filesize unless specified - if (null == result[FILESIZE_COLUMN]) result.put(FILESIZE_COLUMN, 0) - // Smallest possible version unless specified - if (null == result[VERSION_COLUMN]) result.put(VERSION_COLUMN, 1) - // No flags unless specified - if (null == result[FLAGS_COLUMN]) result.put(FLAGS_COLUMN, 0) - return result - } - - /** - * Reads a column in a Cursor as a String and stores it in a ContentValues object. - * @param result the ContentValues object to store the result in. - * @param cursor the Cursor to read the column from. - * @param columnId the column ID to read. - */ - private fun putStringResult(result: ContentValues, cursor: Cursor, columnId: String) { - result.put(columnId, cursor.getString(cursor.getColumnIndex(columnId))) - } - - /** - * Reads a column in a Cursor as an int and stores it in a ContentValues object. - * @param result the ContentValues object to store the result in. - * @param cursor the Cursor to read the column from. - * @param columnId the column ID to read. - */ - private fun putIntResult(result: ContentValues, cursor: Cursor, columnId: String) { - result.put(columnId, cursor.getInt(cursor.getColumnIndex(columnId))) - } - - private fun getFirstLineAsContentValues(cursor: Cursor): ContentValues? { - val result: ContentValues? - if (cursor.moveToFirst()) { - result = ContentValues(COLUMN_COUNT) - putIntResult(result, cursor, PENDINGID_COLUMN) - putIntResult(result, cursor, TYPE_COLUMN) - putIntResult(result, cursor, STATUS_COLUMN) - putStringResult(result, cursor, WORDLISTID_COLUMN) - putStringResult(result, cursor, LOCALE_COLUMN) - putStringResult(result, cursor, DESCRIPTION_COLUMN) - putStringResult(result, cursor, LOCAL_FILENAME_COLUMN) - putStringResult(result, cursor, REMOTE_FILENAME_COLUMN) - putIntResult(result, cursor, DATE_COLUMN) - putStringResult(result, cursor, RAW_CHECKSUM_COLUMN) - putStringResult(result, cursor, CHECKSUM_COLUMN) - putIntResult(result, cursor, RETRY_COUNT_COLUMN) - putIntResult(result, cursor, FILESIZE_COLUMN) - putIntResult(result, cursor, VERSION_COLUMN) - putIntResult(result, cursor, FORMATVERSION_COLUMN) - putIntResult(result, cursor, FLAGS_COLUMN) - if (cursor.moveToNext()) { // TODO: print the second level of the stack to the log so that we know -// in which code path the error happened - Log.e(TAG, "Several SQL results when we expected only one!") - } - } else { - result = null - } - return result - } - - /** - * Gets the info about as specific download, indexed by its DownloadManager ID. - * @param db the database to get the information from. - * @param id the DownloadManager id. - * @return metadata about this download. This returns all columns in the database. - */ - fun getContentValuesByPendingId(db: SQLiteDatabase, - id: Long): ContentValues? { - val cursor = db.query(METADATA_TABLE_NAME, - METADATA_TABLE_COLUMNS, - "$PENDINGID_COLUMN= ?", arrayOf(java.lang.Long.toString(id)), - null, null, null) - ?: return null - return try { // There should never be more than one result. If because of some bug there are, -// returning only one result is the right thing to do, because we couldn't handle -// several anyway and we should still handle one. - getFirstLineAsContentValues(cursor) - } finally { - cursor.close() - } - } - - /** - * Gets the info about an installed OR deleting word list with a specified id. - * - * Basically, this is the word list that we want to return to Android Keyboard when - * it asks for a specific id. - * - * @param db the database to get the information from. - * @param id the word list ID. - * @return the metadata about this word list. - */ - fun getInstalledOrDeletingWordListContentValuesByWordListId( - db: SQLiteDatabase, id: String?): ContentValues? { - val cursor = db.query(METADATA_TABLE_NAME, - METADATA_TABLE_COLUMNS, - "$WORDLISTID_COLUMN=? AND ($STATUS_COLUMN=? OR $STATUS_COLUMN=?)", arrayOf(id, Integer.toString(STATUS_INSTALLED), - Integer.toString(STATUS_DELETING)), - null, null, null) - ?: return null - return try { // There should only be one result, but if there are several, we can't tell which -// is the best, so we just return the first one. - getFirstLineAsContentValues(cursor) - } finally { - cursor.close() - } - } - - /** - * Gets the info about a specific word list. - * - * @param db the database to get the information from. - * @param id the word list ID. - * @param version the word list version. - * @return the metadata about this word list. - */ - fun getContentValuesByWordListId(db: SQLiteDatabase, - id: String?, version: Int): ContentValues? { - val cursor = db.query(METADATA_TABLE_NAME, - METADATA_TABLE_COLUMNS, - WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ? AND " - + FORMATVERSION_COLUMN + "<= ?", arrayOf(id, - Integer.toString(version), - Integer.toString(version) - ), - null /* groupBy */, - null /* having */, - "$FORMATVERSION_COLUMN DESC" /* orderBy */) - ?: return null - return try { // This is a lookup by primary key, so there can't be more than one result. - getFirstLineAsContentValues(cursor) - } finally { - cursor.close() - } - } - - /** - * Gets the info about the latest word list with an id. - * - * @param db the database to get the information from. - * @param id the word list ID. - * @return the metadata about the word list with this id and the latest version number. - */ - fun getContentValuesOfLatestAvailableWordlistById( - db: SQLiteDatabase, id: String): ContentValues? { - val cursor = db.query(METADATA_TABLE_NAME, - METADATA_TABLE_COLUMNS, - "$WORDLISTID_COLUMN= ?", arrayOf(id), null, null, "$VERSION_COLUMN DESC", "1") - ?: return null - return try { // Return the first result from the list of results. - getFirstLineAsContentValues(cursor) - } finally { - cursor.close() - } - } - - /** - * Gets the current metadata about INSTALLED, AVAILABLE or DELETING dictionaries. - * - * This odd method is tailored to the needs of - * DictionaryProvider#getDictionaryWordListsForContentUri, which needs the word list if - * it is: - * - INSTALLED: this should be returned to LatinIME if the file is still inside the dictionary - * pack, so that it can be copied. If the file is not there, it's been copied already and should - * not be returned, so getDictionaryWordListsForContentUri takes care of this. - * - DELETING: this should be returned to LatinIME so that it can actually delete the file. - * - AVAILABLE: this should not be returned, but should be checked for auto-installation. - * - * @param context the context for getting the database. - * @param clientId the client id for retrieving the database. null for default (deprecated) - * @return a cursor with metadata about usable dictionaries. - */ - fun queryInstalledOrDeletingOrAvailableDictionaryMetadata( - context: Context?, clientId: String?): Cursor { // If clientId is null, we get the defaut DB (see #getInstance() for more about this) - return getDb(context, clientId).query(METADATA_TABLE_NAME, - METADATA_TABLE_COLUMNS, - "$STATUS_COLUMN = ? OR $STATUS_COLUMN = ? OR $STATUS_COLUMN = ?", arrayOf(Integer.toString(STATUS_INSTALLED), - Integer.toString(STATUS_DELETING), - Integer.toString(STATUS_AVAILABLE)), - null, null, LOCALE_COLUMN) - } - - /** - * Gets the current metadata about all dictionaries. - * - * This will retrieve the metadata about all dictionaries, including - * older files, or files not yet downloaded. - * - * @param context the context for getting the database. - * @param clientId the client id for retrieving the database. null for default (deprecated) - * @return a cursor with metadata about usable dictionaries. - */ - fun queryCurrentMetadata(context: Context?, clientId: String?): Cursor { // If clientId is null, we get the defaut DB (see #getInstance() for more about this) - return getDb(context, clientId).query(METADATA_TABLE_NAME, - METADATA_TABLE_COLUMNS, null, null, null, null, LOCALE_COLUMN) - } - - /** - * Gets the list of all dictionaries known to the dictionary provider, with only public columns. - * - * This will retrieve information about all known dictionaries, and their status. As such, - * it will also return information about dictionaries on the server that have not been - * downloaded yet, but may be requested. - * This only returns public columns. It does not populate internal columns in the returned - * cursor. - * The value returned by this method is intended to be good to be returned directly for a - * request of the list of dictionaries by a client. - * - * @param context the context to read the database from. - * @param clientId the client id for retrieving the database. null for default (deprecated) - * @return a cursor that lists all available dictionaries and their metadata. - */ - fun queryDictionaries(context: Context?, clientId: String?): Cursor { // If clientId is null, we get the defaut DB (see #getInstance() for more about this) - return getDb(context, clientId).query(METADATA_TABLE_NAME, - DICTIONARIES_LIST_PUBLIC_COLUMNS, // Filter out empty locales so as not to return auxiliary data, like a -// data line for downloading metadata: - "$LOCALE_COLUMN != ?", arrayOf(""), // TODO: Reinstate the following code for bulk, then implement partial updates -/* MetadataDbHelper.TYPE_COLUMN + " = ?", - new String[] { Integer.toString(MetadataDbHelper.TYPE_BULK) }, */ - null, null, LOCALE_COLUMN) - } - - /** - * Deletes all data associated with a client. - * - * @param context the context for opening the database - * @param clientId the ID of the client to delete. - * @return true if the client was successfully deleted, false otherwise. - */ - fun deleteClient(context: Context?, clientId: String?): Boolean { // Remove all metadata associated with this client - val db = getDb(context, clientId) - db.execSQL("DROP TABLE IF EXISTS $METADATA_TABLE_NAME") - db.execSQL(METADATA_TABLE_CREATE) - // Remove this client's entry in the clients table - val defaultDb = getDb(context, "") - return 0 != defaultDb.delete(CLIENT_TABLE_NAME, - "$CLIENT_CLIENT_ID_COLUMN = ?", arrayOf(clientId)) - } - - /** - * Updates information relative to a specific client. - * - * Updatable information includes the metadata URI and the additional ID column. It may be - * expanded in the future. - * The passed values must include a client ID in the key CLIENT_CLIENT_ID_COLUMN, and it must - * be equal to the string passed as an argument for clientId. It may not be empty. - * The passed values must also include a non-null metadata URI in the - * CLIENT_METADATA_URI_COLUMN column, as well as a non-null additional ID in the - * CLIENT_METADATA_ADDITIONAL_ID_COLUMN. Both these strings may be empty. - * If any of the above is not complied with, this function returns without updating data. - * - * @param context the context, to open the database - * @param clientId the ID of the client to update - * @param values the values to update. Must conform to the protocol (see above) - */ - fun updateClientInfo(context: Context?, clientId: String?, - values: ContentValues) { // Sanity check the content values - val valuesClientId = values.getAsString(CLIENT_CLIENT_ID_COLUMN) - val valuesMetadataUri = values.getAsString(CLIENT_METADATA_URI_COLUMN) - val valuesMetadataAdditionalId = values.getAsString(CLIENT_METADATA_ADDITIONAL_ID_COLUMN) - // Empty string is a valid client ID, but external apps may not configure it, so disallow -// both null and empty string. -// Empty string is a valid metadata URI if the client does not want updates, so allow -// empty string but disallow null. -// Empty string is a valid additional ID so allow empty string but disallow null. - if (TextUtils.isEmpty(valuesClientId) || null == valuesMetadataUri || null == valuesMetadataAdditionalId) { // We need all these columns to be filled in - DebugLogUtils.l("Missing parameter for updateClientInfo") - return - } - if (clientId != valuesClientId) { // Mismatch! The client violates the protocol. - DebugLogUtils.l("Received an updateClientInfo request for ", clientId, - " but the values " + "contain a different ID : ", valuesClientId) - return - } - // Default value for a pending ID is NOT_AN_ID - val defaultDb = getDb(context, "") - if (-1L == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) { - defaultDb.update(CLIENT_TABLE_NAME, values, - "$CLIENT_CLIENT_ID_COLUMN = ?", arrayOf(clientId)) - } - } - - /** - * Retrieves the list of existing client IDs. - * @param context the context to open the database - * @return a cursor containing only one column, and one client ID per line. - */ - fun queryClientIds(context: Context?): Cursor { - return getDb(context, null).query(CLIENT_TABLE_NAME, arrayOf(CLIENT_CLIENT_ID_COLUMN), null, null, null, null, null) - } - - /** - * Marks a downloading entry as having successfully downloaded and being installed. - * - * The metadata database contains information about ongoing processes, typically ongoing - * downloads. This marks such an entry as having finished and having installed successfully, - * so it becomes INSTALLED. - * - * @param db the metadata database. - * @param r content values about the entry to mark as processed. - */ - fun markEntryAsFinishedDownloadingAndInstalled(db: SQLiteDatabase, - r: ContentValues) { - when (r.getAsInteger(TYPE_COLUMN)) { - TYPE_BULK -> { - DebugLogUtils.l("Ended processing a wordlist") - // Updating a bulk word list is a three-step operation: -// - Add the new entry to the table -// - Remove the old entry from the table -// - Erase the old file -// We start by gathering the names of the files we should delete. - val filenames: MutableList = LinkedList() - val c = db.query(METADATA_TABLE_NAME, arrayOf(LOCAL_FILENAME_COLUMN), - LOCALE_COLUMN + " = ? AND " + - WORDLISTID_COLUMN + " = ? AND " + STATUS_COLUMN + " = ?", arrayOf(r.getAsString(LOCALE_COLUMN), - r.getAsString(WORDLISTID_COLUMN), - Integer.toString(STATUS_INSTALLED)), - null, null, null) - try { - if (c.moveToFirst()) { // There should never be more than one file, but if there are, it's a bug -// and we should remove them all. I think it might happen if the power of -// the phone is suddenly cut during an update. - val filenameIndex = c.getColumnIndex(LOCAL_FILENAME_COLUMN) - do { - DebugLogUtils.l("Setting for removal", c.getString(filenameIndex)) - filenames.add(c.getString(filenameIndex)) - } while (c.moveToNext()) - } - } finally { - c.close() - } - r.put(STATUS_COLUMN, STATUS_INSTALLED) - db.beginTransactionNonExclusive() - // Delete all old entries. There should never be any stalled entries, but if -// there are, this deletes them. - db.delete(METADATA_TABLE_NAME, - "$WORDLISTID_COLUMN = ?", arrayOf(r.getAsString(WORDLISTID_COLUMN))) - db.insert(METADATA_TABLE_NAME, null, r) - db.setTransactionSuccessful() - db.endTransaction() - for (filename in filenames) { - try { - val f = File(filename) - f.delete() - } catch (e: SecurityException) { // No permissions to delete. Um. Can't do anything. - } // I don't think anything else can be thrown - } - } - else -> { - } - } - } - - /** - * Removes a downloading entry from the database. - * - * This is invoked when a download fails. Either we tried to download, but - * we received a permanent failure and we should remove it, or we got manually - * cancelled and we should leave it at that. - * - * @param db the metadata database. - * @param id the DownloadManager id of the file. - */ - fun deleteDownloadingEntry(db: SQLiteDatabase, id: Long) { - db.delete(METADATA_TABLE_NAME, "$PENDINGID_COLUMN = ? AND $STATUS_COLUMN = ?", arrayOf(java.lang.Long.toString(id), Integer.toString(STATUS_DOWNLOADING))) - } - - /** - * Forcefully removes an entry from the database. - * - * This is invoked when a file is broken. The file has been downloaded, but Android - * Keyboard is telling us it could not open it. - * - * @param db the metadata database. - * @param id the id of the word list. - * @param version the version of the word list. - */ - fun deleteEntry(db: SQLiteDatabase, id: String, version: Int) { - db.delete(METADATA_TABLE_NAME, "$WORDLISTID_COLUMN = ? AND $VERSION_COLUMN = ?", arrayOf(id, Integer.toString(version))) - } - - /** - * Internal method that sets the current status of an entry of the database. - * - * @param db the metadata database. - * @param id the id of the word list. - * @param version the version of the word list. - * @param status the status to set the word list to. - * @param downloadId an optional download id to write, or NOT_A_DOWNLOAD_ID - */ - private fun markEntryAs(db: SQLiteDatabase, id: String?, - version: Int, status: Int, downloadId: Long) { - val values = getContentValuesByWordListId(db, id, version) - values!!.put(STATUS_COLUMN, status) - if (NOT_A_DOWNLOAD_ID != downloadId) { - values.put(PENDINGID_COLUMN, downloadId) - } - db.update(METADATA_TABLE_NAME, values, - "$WORDLISTID_COLUMN = ? AND $VERSION_COLUMN = ?", arrayOf(id, Integer.toString(version))) - } - - /** - * Writes the status column for the wordlist with this id as enabled. Typically this - * means the word list is currently disabled and we want to set its status to INSTALLED. - * - * @param db the metadata database. - * @param id the id of the word list. - * @param version the version of the word list. - */ - fun markEntryAsEnabled(db: SQLiteDatabase, id: String?, - version: Int) { - markEntryAs(db, id, version, STATUS_INSTALLED, NOT_A_DOWNLOAD_ID) - } - - /** - * Writes the status column for the wordlist with this id as disabled. Typically this - * means the word list is currently installed and we want to set its status to DISABLED. - * - * @param db the metadata database. - * @param id the id of the word list. - * @param version the version of the word list. - */ - fun markEntryAsDisabled(db: SQLiteDatabase, id: String?, - version: Int) { - markEntryAs(db, id, version, STATUS_DISABLED, NOT_A_DOWNLOAD_ID) - } - - /** - * Writes the status column for the wordlist with this id as available. This happens for - * example when a word list has been deleted but can be downloaded again. - * - * @param db the metadata database. - * @param id the id of the word list. - * @param version the version of the word list. - */ - fun markEntryAsAvailable(db: SQLiteDatabase, id: String?, - version: Int) { - markEntryAs(db, id, version, STATUS_AVAILABLE, NOT_A_DOWNLOAD_ID) - } - - /** - * Writes the designated word list as downloadable, alongside with its download id. - * - * @param db the metadata database. - * @param id the id of the word list. - * @param version the version of the word list. - * @param downloadId the download id. - */ - fun markEntryAsDownloading(db: SQLiteDatabase, id: String?, - version: Int, downloadId: Long) { - markEntryAs(db, id, version, STATUS_DOWNLOADING, downloadId) - } - - /** - * Writes the designated word list as deleting. - * - * @param db the metadata database. - * @param id the id of the word list. - * @param version the version of the word list. - */ - fun markEntryAsDeleting(db: SQLiteDatabase, id: String?, - version: Int) { - markEntryAs(db, id, version, STATUS_DELETING, NOT_A_DOWNLOAD_ID) - } - - /** - * Checks retry counts and marks the word list as retrying if retry is possible. - * - * @param db the metadata database. - * @param id the id of the word list. - * @param version the version of the word list. - * @return `true` if the retry is possible. - */ - fun maybeMarkEntryAsRetrying(db: SQLiteDatabase, id: String?, - version: Int): Boolean { - val values = getContentValuesByWordListId(db, id, version) - val retryCount = values!!.getAsInteger(RETRY_COUNT_COLUMN) - if (retryCount > 1) { - values.put(STATUS_COLUMN, STATUS_RETRYING) - values.put(RETRY_COUNT_COLUMN, retryCount - 1) - db.update(METADATA_TABLE_NAME, values, - "$WORDLISTID_COLUMN = ? AND $VERSION_COLUMN = ?", arrayOf(id, Integer.toString(version))) - return true - } - return false - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataHandler.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataHandler.kt deleted file mode 100644 index d12ba7c53..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataHandler.kt +++ /dev/null @@ -1,141 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.content.ContentValues -import android.content.Context -import android.database.Cursor -import android.util.Log -import java.io.IOException -import java.io.InputStreamReader -import java.util.* - -/** - * Helper class to easy up manipulation of dictionary pack metadata. - */ -object MetadataHandler { - val TAG = MetadataHandler::class.java.simpleName - // The canonical file name for metadata. This is not the name of a real file on the -// device, but a symbolic name used in the database and in metadata handling. It is never -// tested against, only used for human-readability as the file name for the metadata. - const val METADATA_FILENAME = "metadata.json" - - /** - * Reads the data from the cursor and store it in metadata objects. - * @param results the cursor to read data from. - * @return the constructed list of wordlist metadata. - */ - private fun makeMetadataObject(results: Cursor?): List { - val buildingMetadata = ArrayList() - if (null != results && results.moveToFirst()) { - val localeColumn = results.getColumnIndex(MetadataDbHelper.Companion.LOCALE_COLUMN) - val typeColumn = results.getColumnIndex(MetadataDbHelper.Companion.TYPE_COLUMN) - val descriptionColumn = results.getColumnIndex(MetadataDbHelper.Companion.DESCRIPTION_COLUMN) - val idIndex = results.getColumnIndex(MetadataDbHelper.Companion.WORDLISTID_COLUMN) - val updateIndex = results.getColumnIndex(MetadataDbHelper.Companion.DATE_COLUMN) - val fileSizeIndex = results.getColumnIndex(MetadataDbHelper.Companion.FILESIZE_COLUMN) - val rawChecksumIndex = results.getColumnIndex(MetadataDbHelper.Companion.RAW_CHECKSUM_COLUMN) - val checksumIndex = results.getColumnIndex(MetadataDbHelper.Companion.CHECKSUM_COLUMN) - val retryCountIndex = results.getColumnIndex(MetadataDbHelper.Companion.RETRY_COUNT_COLUMN) - val localFilenameIndex = results.getColumnIndex(MetadataDbHelper.Companion.LOCAL_FILENAME_COLUMN) - val remoteFilenameIndex = results.getColumnIndex(MetadataDbHelper.Companion.REMOTE_FILENAME_COLUMN) - val versionIndex = results.getColumnIndex(MetadataDbHelper.Companion.VERSION_COLUMN) - val formatVersionIndex = results.getColumnIndex(MetadataDbHelper.Companion.FORMATVERSION_COLUMN) - do { - buildingMetadata.add(WordListMetadata(results.getString(idIndex), - results.getInt(typeColumn), - results.getString(descriptionColumn), - results.getLong(updateIndex), - results.getLong(fileSizeIndex), - results.getString(rawChecksumIndex), - results.getString(checksumIndex), - results.getInt(retryCountIndex), - results.getString(localFilenameIndex), - results.getString(remoteFilenameIndex), - results.getInt(versionIndex), - results.getInt(formatVersionIndex), - 0, results.getString(localeColumn))) - } while (results.moveToNext()) - } - return Collections.unmodifiableList(buildingMetadata) - } - - /** - * Gets the whole metadata, for installed and not installed dictionaries. - * @param context The context to open files over. - * @param clientId the client id for retrieving the database. null for default (deprecated) - * @return The current metadata. - */ - fun getCurrentMetadata(context: Context?, - clientId: String?): List { // If clientId is null, we get a cursor on the default database (see -// MetadataDbHelper#getInstance() for more on this) - val results: Cursor = MetadataDbHelper.Companion.queryCurrentMetadata(context, clientId) - // If null, we should return makeMetadataObject(null), so we go through. - return try { - makeMetadataObject(results) - } finally { - results.close() - } - } - - /** - * Gets the metadata, for a specific dictionary. - * - * @param context The context to open files over. - * @param clientId the client id for retrieving the database. null for default (deprecated). - * @param wordListId the word list ID. - * @param version the word list version. - * @return the current metaData - */ - fun getCurrentMetadataForWordList(context: Context?, - clientId: String?, wordListId: String?, version: Int): WordListMetadata? { - val contentValues: ContentValues = MetadataDbHelper.Companion.getContentValuesByWordListId( - MetadataDbHelper.Companion.getDb(context, clientId), wordListId, version)!! - if (contentValues == null) { // TODO: Figure out why this would happen. -// Check if this happens when the metadata gets updated in the background. - Log.e(TAG, String.format("Unable to find the current metadata for wordlist " - + "(clientId=%s, wordListId=%s, version=%d) on the database", - clientId, wordListId, version)) - return null - } - return WordListMetadata.Companion.createFromContentValues(contentValues) - } - - /** - * Read metadata from a stream. - * @param input The stream to read from. - * @return The read metadata. - * @throws IOException if the input stream cannot be read - * @throws BadFormatException if the stream is not in a known format - */ - @Throws(IOException::class, BadFormatException::class) - fun readMetadata(input: InputStreamReader?): List? { - return MetadataParser.parseMetadata(input) - } - - /** - * Finds a single WordListMetadata inside a whole metadata chunk. - * - * Searches through the whole passed metadata for the first WordListMetadata associated - * with the passed ID. If several metadata chunks with the same id are found, it will - * always return the one with the bigger FormatVersion that is less or equal than the - * maximum supported format version (as listed in UpdateHandler). - * This will NEVER return the metadata with a FormatVersion bigger than what is supported, - * even if it is the only word list with this ID. - * - * @param metadata the metadata to search into. - * @param id the word list ID of the metadata to find. - * @return the associated metadata, or null if not found. - */ - fun findWordListById(metadata: List, - id: String): WordListMetadata? { - var bestWordList: WordListMetadata? = null - var bestFormatVersion = Int.MIN_VALUE // To be sure we can't be inadvertently smaller - for (wordList in metadata) { - if (id == wordList.mId && wordList.mFormatVersion > bestFormatVersion) { - bestWordList = wordList - bestFormatVersion = wordList.mFormatVersion - } - } - // If we didn't find any match we'll return null. - return bestWordList - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataParser.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataParser.kt deleted file mode 100644 index 97d6f0ff6..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataParser.kt +++ /dev/null @@ -1,85 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.text.TextUtils -import android.util.JsonReader -import java.io.IOException -import java.io.InputStreamReader -import java.util.* - -/** - * Helper class containing functions to parse the dictionary metadata. - */ -object MetadataParser { - // Name of the fields in the JSON-formatted file. - private val ID_FIELD_NAME: String = MetadataDbHelper.Companion.WORDLISTID_COLUMN - private const val LOCALE_FIELD_NAME = "locale" - private val DESCRIPTION_FIELD_NAME: String = MetadataDbHelper.Companion.DESCRIPTION_COLUMN - private const val UPDATE_FIELD_NAME = "update" - private val FILESIZE_FIELD_NAME: String = MetadataDbHelper.Companion.FILESIZE_COLUMN - private val RAW_CHECKSUM_FIELD_NAME: String = MetadataDbHelper.Companion.RAW_CHECKSUM_COLUMN - private val CHECKSUM_FIELD_NAME: String = MetadataDbHelper.Companion.CHECKSUM_COLUMN - private val REMOTE_FILENAME_FIELD_NAME: String = MetadataDbHelper.Companion.REMOTE_FILENAME_COLUMN - private val VERSION_FIELD_NAME: String = MetadataDbHelper.Companion.VERSION_COLUMN - private val FORMATVERSION_FIELD_NAME: String = MetadataDbHelper.Companion.FORMATVERSION_COLUMN - /** - * Parse one JSON-formatted word list metadata. - * @param reader the reader containing the data. - * @return a WordListMetadata object from the parsed data. - * @throws IOException if the underlying reader throws IOException during reading. - */ - @Throws(IOException::class, BadFormatException::class) - private fun parseOneWordList(reader: JsonReader): WordListMetadata { - val arguments = TreeMap() - reader.beginObject() - while (reader.hasNext()) { - val name = reader.nextName() - if (!TextUtils.isEmpty(name)) { - arguments[name] = reader.nextString() - } - } - reader.endObject() - if (TextUtils.isEmpty(arguments[ID_FIELD_NAME]) - || TextUtils.isEmpty(arguments[LOCALE_FIELD_NAME]) - || TextUtils.isEmpty(arguments[DESCRIPTION_FIELD_NAME]) - || TextUtils.isEmpty(arguments[UPDATE_FIELD_NAME]) - || TextUtils.isEmpty(arguments[FILESIZE_FIELD_NAME]) - || TextUtils.isEmpty(arguments[CHECKSUM_FIELD_NAME]) - || TextUtils.isEmpty(arguments[REMOTE_FILENAME_FIELD_NAME]) - || TextUtils.isEmpty(arguments[VERSION_FIELD_NAME]) - || TextUtils.isEmpty(arguments[FORMATVERSION_FIELD_NAME])) { - throw BadFormatException(arguments.toString()) - } - // TODO: need to find out whether it's bulk or update -// The null argument is the local file name, which is not known at this time and will -// be decided later. - return WordListMetadata( - arguments[ID_FIELD_NAME], - MetadataDbHelper.Companion.TYPE_BULK, - arguments[DESCRIPTION_FIELD_NAME], arguments[UPDATE_FIELD_NAME]!!.toLong(), arguments[FILESIZE_FIELD_NAME]!!.toLong(), - arguments[RAW_CHECKSUM_FIELD_NAME], - arguments[CHECKSUM_FIELD_NAME], - MetadataDbHelper.Companion.DICTIONARY_RETRY_THRESHOLD /* retryCount */, - null, - arguments[REMOTE_FILENAME_FIELD_NAME], arguments[VERSION_FIELD_NAME]!!.toInt(), arguments[FORMATVERSION_FIELD_NAME]!!.toInt(), - 0, arguments[LOCALE_FIELD_NAME]) - } - - /** - * Parses metadata in the JSON format. - * @param input a stream reader expected to contain JSON formatted metadata. - * @return dictionary metadata, as an array of WordListMetadata objects. - * @throws IOException if the underlying reader throws IOException during reading. - * @throws BadFormatException if the data was not in the expected format. - */ - @Throws(IOException::class, BadFormatException::class) - fun parseMetadata(input: InputStreamReader?): List { - val reader = JsonReader(input) - val readInfo = ArrayList() - reader.beginArray() - while (reader.hasNext()) { - val thisMetadata = parseOneWordList(reader) - if (!TextUtils.isEmpty(thisMetadata.mLocale)) readInfo.add(thisMetadata) - } - return Collections.unmodifiableList(readInfo) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataUriGetter.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataUriGetter.kt deleted file mode 100644 index d3ae2968d..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataUriGetter.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.content.Context - -/** - * Helper to get the metadata URI from its base URI. - */ -object MetadataUriGetter { - fun getUri(context: Context?, baseUri: String): String { - return baseUri - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/PrivateLog.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/PrivateLog.kt deleted file mode 100644 index d1c6aab3f..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/PrivateLog.kt +++ /dev/null @@ -1,69 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.content.ContentValues -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import java.text.SimpleDateFormat -import java.util.* - -/** - * Class to keep long-term log. This is inactive in production, and is only for debug purposes. - */ -object PrivateLog { - const val DEBUG = false - private const val LOG_DATABASE_NAME = "log" - private const val LOG_TABLE_NAME = "log" - private const val LOG_DATABASE_VERSION = 1 - private const val COLUMN_DATE = "date" - private const val COLUMN_EVENT = "event" - private const val LOG_TABLE_CREATE = ("CREATE TABLE " + LOG_TABLE_NAME + " (" - + COLUMN_DATE + " TEXT," - + COLUMN_EVENT + " TEXT);") - val sDateFormat = SimpleDateFormat( - "yyyy/MM/dd HH:mm:ss", Locale.ROOT) - private val sInstance: PrivateLog = PrivateLog - private var sDebugHelper: DebugHelper? = null - @Synchronized - fun getInstance(context: Context?): PrivateLog { - if (!DEBUG) return sInstance - synchronized(PrivateLog::class.java) { - if (sDebugHelper == null) { - sDebugHelper = DebugHelper(context) - } - return sInstance - } - } - - fun log(event: String?) { - if (!DEBUG) return - val l = sDebugHelper!!.writableDatabase - DebugHelper.insert(l, event) - } - - internal class DebugHelper(context: Context?) : SQLiteOpenHelper(context, LOG_DATABASE_NAME, null, LOG_DATABASE_VERSION) { - override fun onCreate(db: SQLiteDatabase) { - if (!DEBUG) return - db.execSQL(LOG_TABLE_CREATE) - insert(db, "Created table") - } - - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - if (!DEBUG) return - // Remove all data. - db.execSQL("DROP TABLE IF EXISTS $LOG_TABLE_NAME") - onCreate(db) - insert(db, "Upgrade finished") - } - - companion object { - fun insert(db: SQLiteDatabase, event: String?) { - if (!DEBUG) return - val c = ContentValues(2) - c.put(COLUMN_DATE, sDateFormat.format(Date(System.currentTimeMillis()))) - c.put(COLUMN_EVENT, event) - db.insert(LOG_TABLE_NAME, null, c) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ProblemReporter.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ProblemReporter.kt deleted file mode 100644 index 7237a9ce0..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ProblemReporter.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -/** - * A simple interface to report problems. - */ -interface ProblemReporter { - fun report(e: Exception?) -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListMetadata.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListMetadata.kt deleted file mode 100644 index 7d504bdcc..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListMetadata.kt +++ /dev/null @@ -1,75 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.content.ContentValues - -/** - * The metadata for a single word list. - * - * Instances of this class are always immutable. - */ -class WordListMetadata // In milliseconds -(val mId: String?, // Type, as of MetadataDbHelper#TYPE_* - val mType: Int, - val mDescription: String?, val mLastUpdate: Long, val mFileSize: Long, - val mRawChecksum: String?, val mChecksum: String?, var mRetryCount: Int, - val mLocalFilename: String?, val mRemoteFilename: String?, - // version of this word list - val mVersion: Int, // Version number of the format. -// This implementation of the DictionaryDataService knows how to handle format 1 only. -// This is only for forward compatibility, to be able to upgrade the format without -// breaking old implementations. - val mFormatVersion: Int, - // Always 0 in this version, reserved for future use - val mFlags: Int, // The locale is matched against the locale requested by the client. The matching algorithm -// is a standard locale matching with fallback; it is implemented in -// DictionaryProvider#getDictionaryFileForContentUri. - val mLocale: String?) { - - override fun toString(): String { - val sb = StringBuilder(WordListMetadata::class.java.simpleName) - sb.append(" : ").append(mId) - sb.append("\nType : ").append(mType) - sb.append("\nDescription : ").append(mDescription) - sb.append("\nLastUpdate : ").append(mLastUpdate) - sb.append("\nFileSize : ").append(mFileSize) - sb.append("\nRawChecksum : ").append(mRawChecksum) - sb.append("\nChecksum : ").append(mChecksum) - sb.append("\nRetryCount: ").append(mRetryCount) - sb.append("\nLocalFilename : ").append(mLocalFilename) - sb.append("\nRemoteFilename : ").append(mRemoteFilename) - sb.append("\nVersion : ").append(mVersion) - sb.append("\nFormatVersion : ").append(mFormatVersion) - sb.append("\nFlags : ").append(mFlags) - sb.append("\nLocale : ").append(mLocale) - return sb.toString() - } - - companion object { - /** - * Create a WordListMetadata from the contents of a ContentValues. - * - * If this lacks any required field, IllegalArgumentException is thrown. - */ - fun createFromContentValues(values: ContentValues): WordListMetadata { - val id = values.getAsString(MetadataDbHelper.WORDLISTID_COLUMN) - val type = values.getAsInteger(MetadataDbHelper.TYPE_COLUMN) - val description = values.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN) - val lastUpdate = values.getAsLong(MetadataDbHelper.DATE_COLUMN) - val fileSize = values.getAsLong(MetadataDbHelper.FILESIZE_COLUMN) - val rawChecksum = values.getAsString(MetadataDbHelper.RAW_CHECKSUM_COLUMN) - val checksum = values.getAsString(MetadataDbHelper.CHECKSUM_COLUMN) - val retryCount = values.getAsInteger(MetadataDbHelper.RETRY_COUNT_COLUMN) - val localFilename = values.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN) - val remoteFilename = values.getAsString(MetadataDbHelper.REMOTE_FILENAME_COLUMN) - val version = values.getAsInteger(MetadataDbHelper.VERSION_COLUMN) - val formatVersion = values.getAsInteger(MetadataDbHelper.FORMATVERSION_COLUMN) - val flags = values.getAsInteger(MetadataDbHelper.FLAGS_COLUMN) - val locale = values.getAsString(MetadataDbHelper.LOCALE_COLUMN) - require(!(null == id || null == type || null == description || null == lastUpdate || null == fileSize || null == checksum || null == localFilename || null == remoteFilename || null == version || null == formatVersion || null == flags || null == locale)) - return WordListMetadata(id, type, description, lastUpdate, fileSize, rawChecksum, - checksum, retryCount, localFilename, remoteFilename, version, formatVersion, - flags, locale) - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListPreference.kt b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListPreference.kt deleted file mode 100644 index 7e76b3069..000000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListPreference.kt +++ /dev/null @@ -1,198 +0,0 @@ -package org.dslul.openboard.inputmethod.dictionarypack - -import android.content.Context -import android.preference.Preference -import android.util.Log -import android.view.View -import android.view.ViewGroup -import android.widget.ListView -import org.dslul.openboard.inputmethod.latin.R -import java.util.* - -/** - * A preference for one word list. - * - * This preference refers to a single word list, as available in the dictionary - * pack. Upon being pressed, it displays a menu to allow the user to install, disable, - * enable or delete it as appropriate for the current state of the word list. - */ -@Suppress("deprecation") -class WordListPreference(context: Context?, - private val mInterfaceState: DictionaryListInterfaceState, // The id of the client for which this preference is. - private val mClientId: String?, - // Members -// The metadata word list id and version of this word list. - val mWordlistId: String, val mVersion: Int, val mLocale: Locale, - val mDescription: String, status: Int, // The size of the dictionary file - private val mFilesize: Int) : Preference(context, null) { - // The status - private var mStatus = 0 - - fun setStatus(status: Int) { - if (status == mStatus) return - mStatus = status - summary = getSummary(status) - } - - fun hasStatus(status: Int): Boolean { - return status == mStatus - } - - public override fun onCreateView(parent: ViewGroup): View { - val orphanedView = mInterfaceState.findFirstOrphanedView() - if (null != orphanedView) return orphanedView // Will be sent to onBindView - val newView = super.onCreateView(parent) - return mInterfaceState.addToCacheAndReturnView(newView) - } - - fun hasPriorityOver(otherPrefStatus: Int): Boolean { // Both of these should be one of MetadataDbHelper.STATUS_* - return mStatus > otherPrefStatus - } - - private fun getSummary(status: Int): String { - val context = context - return when (status) { - MetadataDbHelper.STATUS_DELETING, MetadataDbHelper.STATUS_AVAILABLE -> context.getString(R.string.dictionary_available) - MetadataDbHelper.STATUS_DOWNLOADING -> context.getString(R.string.dictionary_downloading) - MetadataDbHelper.STATUS_INSTALLED -> context.getString(R.string.dictionary_installed) - MetadataDbHelper.STATUS_DISABLED -> context.getString(R.string.dictionary_disabled) - else -> NO_STATUS_MESSAGE - } - } - - private fun disableDict() { - val context = context - val prefs = CommonPreferences.getCommonPreferences(context) - CommonPreferences.disable(prefs, mWordlistId) - if (MetadataDbHelper.STATUS_DOWNLOADING == mStatus) { - setStatus(MetadataDbHelper.STATUS_AVAILABLE) - } else if (MetadataDbHelper.STATUS_INSTALLED == mStatus) { // Interface-wise, we should no longer be able to come here. However, this is still -// the right thing to do if we do come here. - setStatus(MetadataDbHelper.STATUS_DISABLED) - } else { - Log.e(TAG, "Unexpected state of the word list for disabling $mStatus") - } - } - - private fun enableDict() { - val context = context - val prefs = CommonPreferences.getCommonPreferences(context) - CommonPreferences.enable(prefs, mWordlistId) - if (MetadataDbHelper.STATUS_AVAILABLE == mStatus) { - setStatus(MetadataDbHelper.STATUS_DOWNLOADING) - } else if (MetadataDbHelper.STATUS_DISABLED == mStatus - || MetadataDbHelper.STATUS_DELETING == mStatus) { // If the status is DELETING, it means Android Keyboard -// has not deleted the word list yet, so we can safely -// turn it to 'installed'. The status DISABLED is still supported internally to -// avoid breaking older installations and all but there should not be a way to -// disable a word list through the interface any more. - setStatus(MetadataDbHelper.STATUS_INSTALLED) - } else { - Log.e(TAG, "Unexpected state of the word list for enabling $mStatus") - } - } - - private fun deleteDict() { - val context = context - val prefs = CommonPreferences.getCommonPreferences(context) - CommonPreferences.disable(prefs, mWordlistId) - setStatus(MetadataDbHelper.STATUS_DELETING) - } - - override fun onBindView(view: View) { - super.onBindView(view) - (view as ViewGroup).layoutTransition = null - val buttonSwitcher = view.findViewById( - R.id.wordlist_button_switcher) as ButtonSwitcher - // We need to clear the state of the button switcher, because we reuse views; if we didn't -// reset it would animate from whatever its old state was. - buttonSwitcher.reset(mInterfaceState) - if (mInterfaceState.isOpen(mWordlistId)) { // The button is open. - val previousStatus = mInterfaceState.getStatus(mWordlistId) - buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(previousStatus)) - if (previousStatus != mStatus) { // We come here if the status has changed since last time. We need to animate -// the transition. - buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus)) - mInterfaceState.setOpen(mWordlistId, mStatus) - } - } else { // The button is closed. - buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON) - } - buttonSwitcher.setInternalOnClickListener(View.OnClickListener { onActionButtonClicked() }) - view.setOnClickListener { v -> onWordListClicked(v) } - } - - fun onWordListClicked(v: View) { // Note : v is the preference view - val listView = v.parent as? ListView ?: return - // Just in case something changed in the framework, test for the concrete class - // Close all first, we'll open back any item that needs to be open. - val wasOpen = mInterfaceState.isOpen(mWordlistId) - mInterfaceState.closeAll() - val indexToOpen = if (wasOpen) { // This button being shown. Take note that we don't want to open any button in the -// loop below. - -1 - } else { // This button was not being shown. Open it, and remember the index of this -// child as the one to open in the following loop. - mInterfaceState.setOpen(mWordlistId, mStatus) - listView.indexOfChild(v) - } - val lastDisplayedIndex = listView.lastVisiblePosition - listView.firstVisiblePosition - // The "lastDisplayedIndex" is actually displayed, hence the <= - for (i in 0..lastDisplayedIndex) { - val buttonSwitcher = listView.getChildAt(i) - .findViewById(R.id.wordlist_button_switcher) as ButtonSwitcher - if (i == indexToOpen) { - buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus)) - } else { - buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.Companion.STATUS_NO_BUTTON) - } - } - } - - fun onActionButtonClicked() { - when (getActionIdFromStatusAndMenuEntry(mStatus)) { - ACTION_ENABLE_DICT -> enableDict() - ACTION_DISABLE_DICT -> disableDict() - ACTION_DELETE_DICT -> deleteDict() - else -> Log.e(TAG, "Unknown menu item pressed") - } - } - - companion object { - private val TAG = WordListPreference::class.java.simpleName - // What to display in the "status" field when we receive unknown data as a status from -// the content provider. Empty string sounds sensible. - private const val NO_STATUS_MESSAGE = "" - /// Actions - private const val ACTION_UNKNOWN = 0 - private const val ACTION_ENABLE_DICT = 1 - private const val ACTION_DISABLE_DICT = 2 - private const val ACTION_DELETE_DICT = 3 - // The table below needs to be kept in sync with MetadataDbHelper.STATUS_* since it uses -// the values as indices. - private val sStatusActionList = arrayOf(intArrayOf(), intArrayOf(ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT), intArrayOf(ButtonSwitcher.Companion.STATUS_CANCEL, ACTION_DISABLE_DICT), intArrayOf(ButtonSwitcher.Companion.STATUS_DELETE, ACTION_DELETE_DICT), intArrayOf(ButtonSwitcher.Companion.STATUS_DELETE, ACTION_DELETE_DICT), intArrayOf(ButtonSwitcher.Companion.STATUS_INSTALL, ACTION_ENABLE_DICT)) - - fun getButtonSwitcherStatus(status: Int): Int { - if (status >= sStatusActionList.size) { - Log.e(TAG, "Unknown status $status") - return ButtonSwitcher.STATUS_NO_BUTTON - } - return sStatusActionList[status][0] - } - - fun getActionIdFromStatusAndMenuEntry(status: Int): Int { - if (status >= sStatusActionList.size) { - Log.e(TAG, "Unknown status $status") - return ACTION_UNKNOWN - } - return sStatusActionList[status][1] - } - } - - init { - layoutResource = R.layout.dictionary_line - title = mDescription - setStatus(status) - key = mWordlistId - } -} \ No newline at end of file diff --git a/app/src/main/res/layout/dictionary_line.xml b/app/src/main/res/layout/dictionary_line.xml deleted file mode 100644 index 840829de2..000000000 --- a/app/src/main/res/layout/dictionary_line.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - -