mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-05-01 20:34:30 +00:00
remove old unused dictionary pack settings
This commit is contained in:
parent
25b1c628e3
commit
0b92c6ce0d
20 changed files with 2 additions and 3072 deletions
|
@ -1,24 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
/*
|
|
||||||
**
|
|
||||||
** Copyright 2013, The Android Open Source Project
|
|
||||||
**
|
|
||||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
** you may not use this file except in compliance with the License.
|
|
||||||
** You may obtain a copy of the License at
|
|
||||||
**
|
|
||||||
** http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
**
|
|
||||||
** Unless required by applicable law or agreed to in writing, software
|
|
||||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
** See the License for the specific language governing permissions and
|
|
||||||
** limitations under the License.
|
|
||||||
*/
|
|
||||||
-->
|
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
|
||||||
<string name="dictionary_pack_client_id" translatable="false">org.dslul.openboard.inputmethod.latin.debug</string>
|
|
||||||
<!-- <string name="dictionary_pack_settings_activity" translatable="false">org.dslul.openboard.inputmethod.dictionarypack.DictionarySettingsActivity</string> -->
|
|
||||||
<string name="authority" translatable="false">org.dslul.openboard.inputmethod.debug.dictionarypack.aosp</string>
|
|
||||||
</resources>
|
|
|
@ -160,12 +160,13 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<!-- Content providers -->
|
<!-- Content providers -->
|
||||||
|
<!--
|
||||||
<provider android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryProvider"
|
<provider android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryProvider"
|
||||||
android:grantUriPermissions="true"
|
android:grantUriPermissions="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:authorities="@string/authority"
|
android:authorities="@string/authority"
|
||||||
android:multiprocess="false"
|
android:multiprocess="false"
|
||||||
android:label="@string/dictionary_provider_name">
|
android:label="@string/dictionary_provider_name">
|
||||||
</provider>
|
</provider>-->
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -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<Action>
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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<View>(R.id.dict_install_button) as Button
|
|
||||||
mCancelButton = findViewById<View>(R.id.dict_cancel_button) as Button
|
|
||||||
mDeleteButton = findViewById<View>(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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<String, State>()
|
|
||||||
private val mViewCache = ArrayList<View>()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<WordListInfo>) : AbstractCursor() {
|
|
||||||
// The list of word lists served by this provider that match the client request.
|
|
||||||
val mWordLists: Array<WordListInfo>
|
|
||||||
|
|
||||||
override fun getColumnNames(): Array<String> {
|
|
||||||
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<String>(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<String>?, selection: String?,
|
|
||||||
selectionArgs: Array<String>?, 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<WordListInfo> {
|
|
||||||
val context = context
|
|
||||||
val results: Cursor = MetadataDbHelper.queryInstalledOrDeletingOrAvailableDictionaryMetadata(context,
|
|
||||||
clientId)
|
|
||||||
return try {
|
|
||||||
val dicts = HashMap<String, WordListInfo>()
|
|
||||||
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<String>?): 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<String>?): Int {
|
|
||||||
PrivateLog.log("Attempt to update : $uri")
|
|
||||||
throw UnsupportedOperationException("Updating dictionary words is not supported")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<String, WordListPreference>()
|
|
||||||
|
|
||||||
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<Preference> { // 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<Preference>()
|
|
||||||
result.add(createErrorMessage(activity, R.string.cannot_connect_to_dict_service))
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return try {
|
|
||||||
if (!cursor.moveToFirst()) {
|
|
||||||
val result = ArrayList<Preference>()
|
|
||||||
result.add(createErrorMessage(activity, R.string.no_dictionaries_available))
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
val systemLocaleString = Locale.getDefault().toString()
|
|
||||||
val prefMap = TreeMap<String, WordListPreference>()
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<String, MetadataDbHelper>? = 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<String> = 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<WordListMetadata> {
|
|
||||||
val buildingMetadata = ArrayList<WordListMetadata>()
|
|
||||||
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<WordListMetadata> { // 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<WordListMetadata?>? {
|
|
||||||
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<WordListMetadata>,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<String, String>()
|
|
||||||
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<WordListMetadata> {
|
|
||||||
val reader = JsonReader(input)
|
|
||||||
val readInfo = ArrayList<WordListMetadata>()
|
|
||||||
reader.beginArray()
|
|
||||||
while (reader.hasNext()) {
|
|
||||||
val thisMetadata = parseOneWordList(reader)
|
|
||||||
if (!TextUtils.isEmpty(thisMetadata.mLocale)) readInfo.add(thisMetadata)
|
|
||||||
}
|
|
||||||
return Collections.unmodifiableList(readInfo)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package org.dslul.openboard.inputmethod.dictionarypack
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple interface to report problems.
|
|
||||||
*/
|
|
||||||
interface ProblemReporter {
|
|
||||||
fun report(e: Exception?)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<View>(
|
|
||||||
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<View>(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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
/*
|
|
||||||
**
|
|
||||||
** Copyright 2012, The Android Open Source Project
|
|
||||||
**
|
|
||||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
** you may not use this file except in compliance with the License.
|
|
||||||
** You may obtain a copy of the License at
|
|
||||||
**
|
|
||||||
** http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
**
|
|
||||||
** Unless required by applicable law or agreed to in writing, software
|
|
||||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
** See the License for the specific language governing permissions and
|
|
||||||
** limitations under the License.
|
|
||||||
*/
|
|
||||||
-->
|
|
||||||
<!-- TODO: Remove paddingRight, layout_marginLeft, layout_marginRight for API version 17+ -->
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:paddingRight="?android:attr/scrollbarSize"
|
|
||||||
android:paddingEnd="?android:attr/scrollbarSize"
|
|
||||||
android:background="?android:attr/selectableItemBackground"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="15dip"
|
|
||||||
android:layout_marginStart="15dip"
|
|
||||||
android:layout_marginRight="6dip"
|
|
||||||
android:layout_marginEnd="6dip"
|
|
||||||
android:layout_marginTop="6dip"
|
|
||||||
android:layout_marginBottom="6dip"
|
|
||||||
android:layout_weight="1">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@android:id/title"
|
|
||||||
android:layout_marginLeft="5dip"
|
|
||||||
android:layout_marginStart="5dip"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
android:ellipsize="marquee"
|
|
||||||
android:fadingEdge="horizontal" />
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="5dip"
|
|
||||||
android:layout_marginLeft="5dip">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@android:id/summary"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
|
||||||
android:maxLines="1" />
|
|
||||||
|
|
||||||
<org.dslul.openboard.inputmethod.dictionarypack.DictionaryDownloadProgressBar
|
|
||||||
android:id="@+id/dictionary_line_progress_bar"
|
|
||||||
style="@android:style/Widget.Holo.ProgressBar.Horizontal"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<org.dslul.openboard.inputmethod.dictionarypack.ButtonSwitcher
|
|
||||||
android:id="@+id/wordlist_button_switcher"
|
|
||||||
android:layout_weight="0"
|
|
||||||
android:layout_marginStart="13dip"
|
|
||||||
android:layout_marginLeft="13dip"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<Button
|
|
||||||
android:id="@+id/dict_install_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="right|center_vertical"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
android:text="@string/install_dict" />
|
|
||||||
<Button
|
|
||||||
android:id="@+id/dict_cancel_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="right|center_vertical"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
android:text="@android:string/cancel" />
|
|
||||||
<Button
|
|
||||||
android:id="@+id/dict_delete_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="right|center_vertical"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
android:text="@string/delete_dict" />
|
|
||||||
</org.dslul.openboard.inputmethod.dictionarypack.ButtonSwitcher>
|
|
||||||
</LinearLayout>
|
|
|
@ -20,8 +20,5 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="dictionary_pack_client_id" translatable="false">org.dslul.openboard.inputmethod.latin</string>
|
<string name="dictionary_pack_client_id" translatable="false">org.dslul.openboard.inputmethod.latin</string>
|
||||||
<string name="dictionary_pack_metadata_uri" translatable="false"></string>
|
<string name="dictionary_pack_metadata_uri" translatable="false"></string>
|
||||||
<string name="dictionary_pack_settings_activity" translatable="false">org.dslul.openboard.inputmethod.dictionarypack.DictionarySettingsActivity</string>
|
|
||||||
<string name="authority" translatable="false">org.dslul.openboard.inputmethod.dictionarypack.aosp</string>
|
|
||||||
<string name="default_metadata_uri" translatable="false"></string>
|
|
||||||
<string name="local_metadata_filename" translatable="false">metadata.json</string>
|
<string name="local_metadata_filename" translatable="false">metadata.json</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Add table
Reference in a new issue