remove old unused dictionary pack settings

This commit is contained in:
Helium314 2023-09-07 09:19:47 +02:00
parent 25b1c628e3
commit 0b92c6ce0d
20 changed files with 2 additions and 3072 deletions

View file

@ -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>

View file

@ -160,12 +160,13 @@
</receiver>
<!-- Content providers -->
<!--
<provider android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryProvider"
android:grantUriPermissions="true"
android:exported="false"
android:authorities="@string/authority"
android:multiprocess="false"
android:label="@string/dictionary_provider_name">
</provider>
</provider>-->
</application>
</manifest>

View file

@ -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()
}
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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")
}
}

View file

@ -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))
}
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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)
}
}
}
}

View file

@ -1,8 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
/**
* A simple interface to report problems.
*/
interface ProblemReporter {
fun report(e: Exception?)
}

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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>

View file

@ -20,8 +20,5 @@
<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_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>
</resources>