mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-05-14 05:52:47 +00:00
convert dictionarypack to kotlin
This commit is contained in:
parent
faff1e2ba7
commit
23c54876b6
35 changed files with 3258 additions and 3685 deletions
|
@ -1,512 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
|
||||
import android.app.DownloadManager.Request;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryFileDumper;
|
||||
import org.dslul.openboard.inputmethod.latin.R;
|
||||
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||
import org.dslul.openboard.inputmethod.latin.utils.ApplicationUtils;
|
||||
import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public final class ActionBatch {
|
||||
/**
|
||||
* A piece of update.
|
||||
*
|
||||
* Action is basically like a Runnable that takes an argument.
|
||||
*/
|
||||
public interface Action {
|
||||
/**
|
||||
* Execute this action NOW.
|
||||
* @param context the context to get system services, resources, databases
|
||||
*/
|
||||
void execute(final Context context);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An action that enables an existing word list.
|
||||
*/
|
||||
public static final class EnableAction implements Action {
|
||||
static final String TAG = "DictionaryProvider:" + EnableAction.class.getSimpleName();
|
||||
private final String mClientId;
|
||||
// The state to upgrade from. May not be null.
|
||||
final WordListMetadata mWordList;
|
||||
|
||||
public EnableAction(final String clientId, final WordListMetadata wordList) {
|
||||
DebugLogUtils.l("New EnableAction for client ", clientId, " : ", wordList);
|
||||
mClientId = clientId;
|
||||
mWordList = wordList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final Context context) {
|
||||
if (null == mWordList) {
|
||||
Log.e(TAG, "EnableAction with a null parameter!");
|
||||
return;
|
||||
}
|
||||
DebugLogUtils.l("Enabling word list");
|
||||
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
|
||||
mWordList.mId, mWordList.mVersion);
|
||||
final int 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An action that disables a word list.
|
||||
*/
|
||||
public static final class DisableAction implements Action {
|
||||
static final String TAG = "DictionaryProvider:" + DisableAction.class.getSimpleName();
|
||||
private final String mClientId;
|
||||
// The word list to disable. May not be null.
|
||||
final WordListMetadata mWordList;
|
||||
public DisableAction(final String clientId, final WordListMetadata wordlist) {
|
||||
DebugLogUtils.l("New Disable action for client ", clientId, " : ", wordlist);
|
||||
mClientId = clientId;
|
||||
mWordList = wordlist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final 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);
|
||||
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
|
||||
mWordList.mId, mWordList.mVersion);
|
||||
final int 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".
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An action that makes a word list available.
|
||||
*/
|
||||
public static final class MakeAvailableAction implements Action {
|
||||
static final String TAG = "DictionaryProvider:" + MakeAvailableAction.class.getSimpleName();
|
||||
private final String mClientId;
|
||||
// The word list to make available. May not be null.
|
||||
final WordListMetadata mWordList;
|
||||
public MakeAvailableAction(final String clientId, final WordListMetadata wordlist) {
|
||||
DebugLogUtils.l("New MakeAvailable action", clientId, " : ", wordlist);
|
||||
mClientId = clientId;
|
||||
mWordList = wordlist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final Context context) {
|
||||
if (null == mWordList) { // This should never happen
|
||||
Log.e(TAG, "MakeAvailableAction with a null word list!");
|
||||
return;
|
||||
}
|
||||
final SQLiteDatabase db = 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.
|
||||
final ContentValues values = MetadataDbHelper.makeContentValues(0,
|
||||
MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_AVAILABLE,
|
||||
mWordList.mId, mWordList.mLocale, mWordList.mDescription,
|
||||
null == mWordList.mLocalFilename ? "" : 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static final class MarkPreInstalledAction implements Action {
|
||||
static final String TAG = "DictionaryProvider:"
|
||||
+ MarkPreInstalledAction.class.getSimpleName();
|
||||
private final String mClientId;
|
||||
// The word list to mark pre-installed. May not be null.
|
||||
final WordListMetadata mWordList;
|
||||
public MarkPreInstalledAction(final String clientId, final WordListMetadata wordlist) {
|
||||
DebugLogUtils.l("New MarkPreInstalled action", clientId, " : ", wordlist);
|
||||
mClientId = clientId;
|
||||
mWordList = wordlist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final Context context) {
|
||||
if (null == mWordList) { // This should never happen
|
||||
Log.e(TAG, "MarkPreInstalledAction with a null word list!");
|
||||
return;
|
||||
}
|
||||
final SQLiteDatabase db = 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.
|
||||
final ContentValues values = MetadataDbHelper.makeContentValues(0,
|
||||
MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_INSTALLED,
|
||||
mWordList.mId, mWordList.mLocale, mWordList.mDescription,
|
||||
TextUtils.isEmpty(mWordList.mLocalFilename) ? "" : 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An action that updates information about a word list - description, locale etc
|
||||
*/
|
||||
public static final class UpdateDataAction implements Action {
|
||||
static final String TAG = "DictionaryProvider:" + UpdateDataAction.class.getSimpleName();
|
||||
private final String mClientId;
|
||||
final WordListMetadata mWordList;
|
||||
public UpdateDataAction(final String clientId, final WordListMetadata wordlist) {
|
||||
DebugLogUtils.l("New UpdateData action for client ", clientId, " : ", wordlist);
|
||||
mClientId = clientId;
|
||||
mWordList = wordlist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final Context context) {
|
||||
if (null == mWordList) { // This should never happen
|
||||
Log.e(TAG, "UpdateDataAction with a null word list!");
|
||||
return;
|
||||
}
|
||||
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||
ContentValues oldValues = 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);
|
||||
final ContentValues values = 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 + " = ?",
|
||||
new String[] { mWordList.mId, Integer.toString(mWordList.mVersion) });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static final class ForgetAction implements Action {
|
||||
static final String TAG = "DictionaryProvider:" + ForgetAction.class.getSimpleName();
|
||||
private final String mClientId;
|
||||
// The word list to remove. May not be null.
|
||||
final WordListMetadata mWordList;
|
||||
final boolean mHasNewerVersion;
|
||||
public ForgetAction(final String clientId, final WordListMetadata wordlist,
|
||||
final boolean hasNewerVersion) {
|
||||
DebugLogUtils.l("New TryRemove action for client ", clientId, " : ", wordlist);
|
||||
mClientId = clientId;
|
||||
mWordList = wordlist;
|
||||
mHasNewerVersion = hasNewerVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final 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);
|
||||
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||
final ContentValues values = 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;
|
||||
}
|
||||
final int 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 + " = ?",
|
||||
new String[] { 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 + " = ?",
|
||||
new String[] { mWordList.mId, Integer.toString(mWordList.mVersion) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static final class StartDeleteAction implements Action {
|
||||
static final String TAG = "DictionaryProvider:" + StartDeleteAction.class.getSimpleName();
|
||||
private final String mClientId;
|
||||
// The word list to delete. May not be null.
|
||||
final WordListMetadata mWordList;
|
||||
public StartDeleteAction(final String clientId, final WordListMetadata wordlist) {
|
||||
DebugLogUtils.l("New StartDelete action for client ", clientId, " : ", wordlist);
|
||||
mClientId = clientId;
|
||||
mWordList = wordlist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final 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);
|
||||
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
|
||||
mWordList.mId, mWordList.mVersion);
|
||||
if (null == values) {
|
||||
Log.e(TAG, "Trying to set a non-existing wordlist for removal. Cancelling.");
|
||||
return;
|
||||
}
|
||||
final int 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static final class FinishDeleteAction implements Action {
|
||||
static final String TAG = "DictionaryProvider:" + FinishDeleteAction.class.getSimpleName();
|
||||
private final String mClientId;
|
||||
// The word list to delete. May not be null.
|
||||
final WordListMetadata mWordList;
|
||||
public FinishDeleteAction(final String clientId, final WordListMetadata wordlist) {
|
||||
DebugLogUtils.l("New FinishDelete action for client", clientId, " : ", wordlist);
|
||||
mClientId = clientId;
|
||||
mWordList = wordlist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final 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);
|
||||
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
|
||||
mWordList.mId, mWordList.mVersion);
|
||||
if (null == values) {
|
||||
Log.e(TAG, "Trying to set a non-existing wordlist for removal. Cancelling.");
|
||||
return;
|
||||
}
|
||||
final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||
if (MetadataDbHelper.STATUS_DELETING != status) {
|
||||
Log.e(TAG, "Unexpected status for finish-deleting a word list info : " + status);
|
||||
}
|
||||
final String 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 + " = ?",
|
||||
new String[] { mWordList.mId, Integer.toString(mWordList.mVersion) });
|
||||
} else {
|
||||
MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An action batch consists of an ordered queue of Actions that can execute.
|
||||
private final Queue<Action> mActions;
|
||||
|
||||
public ActionBatch() {
|
||||
mActions = new LinkedList<>();
|
||||
}
|
||||
|
||||
public void add(final Action a) {
|
||||
mActions.add(a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append all the actions of another action batch.
|
||||
* @param that the upgrade to merge into this one.
|
||||
*/
|
||||
public void append(final ActionBatch that) {
|
||||
for (final Action a : 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.
|
||||
*/
|
||||
public void execute(final Context context, final ProblemReporter reporter) {
|
||||
DebugLogUtils.l("Executing a batch of actions");
|
||||
Queue<Action> remainingActions = mActions;
|
||||
while (!remainingActions.isEmpty()) {
|
||||
final Action a = remainingActions.poll();
|
||||
try {
|
||||
a.execute(context);
|
||||
} catch (Exception e) {
|
||||
if (null != reporter)
|
||||
reporter.report(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,517 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
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.Companion.getDb(context, mClientId)
|
||||
val values: ContentValues = MetadataDbHelper.Companion.getContentValuesByWordListId(db,
|
||||
mWordList.mId, mWordList.mVersion)!!
|
||||
val status = values.getAsInteger(MetadataDbHelper.Companion.STATUS_COLUMN)
|
||||
if (MetadataDbHelper.Companion.STATUS_DISABLED != status
|
||||
&& MetadataDbHelper.Companion.STATUS_DELETING != status) {
|
||||
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + " : " + status
|
||||
+ " for an enable action. Cancelling")
|
||||
return
|
||||
}
|
||||
MetadataDbHelper.Companion.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.Companion.getDb(context, mClientId)
|
||||
val values: ContentValues = MetadataDbHelper.Companion.getContentValuesByWordListId(db,
|
||||
mWordList.mId, mWordList.mVersion)!!
|
||||
val status = values.getAsInteger(MetadataDbHelper.Companion.STATUS_COLUMN)
|
||||
if (MetadataDbHelper.Companion.STATUS_INSTALLED == status) { // Disabling an installed word list
|
||||
MetadataDbHelper.Companion.markEntryAsDisabled(db, mWordList.mId, mWordList.mVersion)
|
||||
} else {
|
||||
if (MetadataDbHelper.Companion.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.Companion.getDb(context, mClientId)
|
||||
if (null != MetadataDbHelper.Companion.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.Companion.makeContentValues(0,
|
||||
MetadataDbHelper.Companion.TYPE_BULK, MetadataDbHelper.Companion.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.Companion.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.Companion.getDb(context, mClientId)
|
||||
if (null != MetadataDbHelper.Companion.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.Companion.makeContentValues(0,
|
||||
MetadataDbHelper.Companion.TYPE_BULK, MetadataDbHelper.Companion.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.Companion.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.Companion.getDb(context, mClientId)
|
||||
val oldValues: ContentValues = MetadataDbHelper.Companion.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.Companion.makeContentValues(
|
||||
oldValues.getAsInteger(MetadataDbHelper.Companion.PENDINGID_COLUMN),
|
||||
oldValues.getAsInteger(MetadataDbHelper.Companion.TYPE_COLUMN),
|
||||
oldValues.getAsInteger(MetadataDbHelper.Companion.STATUS_COLUMN),
|
||||
mWordList.mId, mWordList.mLocale, mWordList.mDescription,
|
||||
oldValues.getAsString(MetadataDbHelper.Companion.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.Companion.METADATA_TABLE_NAME, values,
|
||||
MetadataDbHelper.Companion.WORDLISTID_COLUMN + " = ? AND "
|
||||
+ MetadataDbHelper.Companion.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.Companion.getDb(context, mClientId)
|
||||
val values: ContentValues = MetadataDbHelper.Companion.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.Companion.STATUS_COLUMN)
|
||||
if (mHasNewerVersion && MetadataDbHelper.Companion.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.Companion.STATUS_INSTALLED == status || MetadataDbHelper.Companion.STATUS_DISABLED == status || MetadataDbHelper.Companion.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.Companion.REMOTE_FILENAME_COLUMN, "")
|
||||
values.put(MetadataDbHelper.Companion.STATUS_COLUMN, MetadataDbHelper.Companion.STATUS_DELETING)
|
||||
db.update(MetadataDbHelper.Companion.METADATA_TABLE_NAME, values,
|
||||
MetadataDbHelper.Companion.WORDLISTID_COLUMN + " = ? AND "
|
||||
+ MetadataDbHelper.Companion.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.Companion.METADATA_TABLE_NAME,
|
||||
MetadataDbHelper.Companion.WORDLISTID_COLUMN + " = ? AND "
|
||||
+ MetadataDbHelper.Companion.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.Companion.getDb(context, mClientId)
|
||||
val values: ContentValues = MetadataDbHelper.Companion.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.Companion.STATUS_COLUMN)
|
||||
if (MetadataDbHelper.Companion.STATUS_DISABLED != status) {
|
||||
Log.e(TAG, "Unexpected status for deleting a word list info : $status")
|
||||
}
|
||||
MetadataDbHelper.Companion.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.Companion.getDb(context, mClientId)
|
||||
val values: ContentValues = MetadataDbHelper.Companion.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.Companion.STATUS_COLUMN)
|
||||
if (MetadataDbHelper.Companion.STATUS_DELETING != status) {
|
||||
Log.e(TAG, "Unexpected status for finish-deleting a word list info : $status")
|
||||
}
|
||||
val remoteFilename = values.getAsString(MetadataDbHelper.Companion.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.Companion.METADATA_TABLE_NAME,
|
||||
MetadataDbHelper.Companion.WORDLISTID_COLUMN + " = ? AND "
|
||||
+ MetadataDbHelper.Companion.VERSION_COLUMN + " = ?", arrayOf(mWordList.mId, Integer.toString(mWordList.mVersion)))
|
||||
} else {
|
||||
MetadataDbHelper.Companion.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = "DictionaryProvider:" + FinishDeleteAction::class.java.simpleName
|
||||
}
|
||||
|
||||
init {
|
||||
DebugLogUtils.l("New FinishDelete action for client", clientId, " : ", wordlist)
|
||||
mClientId = clientId
|
||||
mWordList = wordlist
|
||||
}
|
||||
}
|
||||
|
||||
// An action batch consists of an ordered queue of Actions that can execute.
|
||||
private val mActions: Queue<Action>
|
||||
|
||||
fun add(a: Action) {
|
||||
mActions.add(a)
|
||||
}
|
||||
|
||||
/**
|
||||
* Append all the actions of another action batch.
|
||||
* @param that the upgrade to merge into this one.
|
||||
*/
|
||||
fun append(that: ActionBatch) {
|
||||
for (a in that.mActions) {
|
||||
add(a)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute this batch.
|
||||
*
|
||||
* @param context the context for getting resources, databases, system services.
|
||||
* @param reporter a Reporter to send errors to.
|
||||
*/
|
||||
fun execute(context: Context?, reporter: ProblemReporter?) {
|
||||
DebugLogUtils.l("Executing a batch of actions")
|
||||
val remainingActions = mActions
|
||||
while (!remainingActions.isEmpty()) {
|
||||
val a = remainingActions.poll()
|
||||
try {
|
||||
a.execute(context)
|
||||
} catch (e: Exception) {
|
||||
reporter?.report(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
mActions = LinkedList()
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Immutable class to hold the address of an asset.
|
||||
* As opposed to a normal file, an asset is usually represented as a contiguous byte array in
|
||||
* the package file. Open it correctly thus requires the name of the package it is in, but
|
||||
* also the offset in the file and the length of this data. This class encapsulates these three.
|
||||
*/
|
||||
final class AssetFileAddress {
|
||||
public final String mFilename;
|
||||
public final long mOffset;
|
||||
public final long mLength;
|
||||
|
||||
public AssetFileAddress(final String filename, final long offset, final long length) {
|
||||
mFilename = filename;
|
||||
mOffset = offset;
|
||||
mLength = length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an AssetFileAddress. This may return null.
|
||||
*
|
||||
* @param filename the filename.
|
||||
* @return the address, or null if the file does not exist or the parameters are not valid.
|
||||
*/
|
||||
public static AssetFileAddress makeFromFileName(final String filename) {
|
||||
if (null == filename) return null;
|
||||
final File f = new File(filename);
|
||||
if (!f.isFile()) return null;
|
||||
return new AssetFileAddress(filename, 0l, f.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an AssetFileAddress. This may return null.
|
||||
*
|
||||
* @param filename the filename.
|
||||
* @param offset the offset.
|
||||
* @param length the length.
|
||||
* @return the address, or null if the file does not exist or the parameters are not valid.
|
||||
*/
|
||||
public static AssetFileAddress makeFromFileNameAndOffset(final String filename,
|
||||
final long offset, final long length) {
|
||||
if (null == filename) return null;
|
||||
final File f = new File(filename);
|
||||
if (!f.isFile()) return null;
|
||||
return new AssetFileAddress(filename, offset, length);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
package org.dslul.openboard.inputmethod.dictionarypack
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Immutable class to hold the address of an asset.
|
||||
* As opposed to a normal file, an asset is usually represented as a contiguous byte array in
|
||||
* the package file. Open it correctly thus requires the name of the package it is in, but
|
||||
* also the offset in the file and the length of this data. This class encapsulates these three.
|
||||
*/
|
||||
internal class AssetFileAddress(val mFilename: String, val mOffset: Long, val mLength: Long) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Makes an AssetFileAddress. This may return null.
|
||||
*
|
||||
* @param filename the filename.
|
||||
* @return the address, or null if the file does not exist or the parameters are not valid.
|
||||
*/
|
||||
fun makeFromFileName(filename: String?): AssetFileAddress? {
|
||||
if (null == filename) return null
|
||||
val f = File(filename)
|
||||
return if (!f.isFile) null else AssetFileAddress(filename, 0L, f.length())
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an AssetFileAddress. This may return null.
|
||||
*
|
||||
* @param filename the filename.
|
||||
* @param offset the offset.
|
||||
* @param length the length.
|
||||
* @return the address, or null if the file does not exist or the parameters are not valid.
|
||||
*/
|
||||
fun makeFromFileNameAndOffset(filename: String?,
|
||||
offset: Long, length: Long): AssetFileAddress? {
|
||||
if (null == filename) return null
|
||||
val f = File(filename)
|
||||
return if (!f.isFile) null else AssetFileAddress(filename, offset, length)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -13,18 +13,12 @@
|
|||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
package org.dslul.openboard.inputmethod.dictionarypack
|
||||
|
||||
/**
|
||||
* Exception thrown when the metadata for the dictionary does not comply to a known format.
|
||||
*/
|
||||
public final class BadFormatException extends Exception {
|
||||
public BadFormatException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public BadFormatException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
class BadFormatException : Exception {
|
||||
constructor() : super() {}
|
||||
constructor(message: String?) : super(message) {}
|
||||
}
|
|
@ -1,170 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
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.
|
||||
*/
|
||||
public class ButtonSwitcher extends FrameLayout {
|
||||
public static final int NOT_INITIALIZED = -1;
|
||||
public static final int STATUS_NO_BUTTON = 0;
|
||||
public static final int STATUS_INSTALL = 1;
|
||||
public static final int STATUS_CANCEL = 2;
|
||||
public static final int STATUS_DELETE = 3;
|
||||
// One of the above
|
||||
private int mStatus = NOT_INITIALIZED;
|
||||
private int mAnimateToStatus = NOT_INITIALIZED;
|
||||
|
||||
// Animation directions
|
||||
public static final int ANIMATION_IN = 1;
|
||||
public static final int ANIMATION_OUT = 2;
|
||||
|
||||
private Button mInstallButton;
|
||||
private Button mCancelButton;
|
||||
private Button mDeleteButton;
|
||||
private DictionaryListInterfaceState mInterfaceState;
|
||||
private OnClickListener mOnClickListener;
|
||||
|
||||
public ButtonSwitcher(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public ButtonSwitcher(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
public void reset(final DictionaryListInterfaceState interfaceState) {
|
||||
mStatus = NOT_INITIALIZED;
|
||||
mAnimateToStatus = NOT_INITIALIZED;
|
||||
mInterfaceState = interfaceState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(final boolean changed, final int left, final int top, final int right,
|
||||
final int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
mInstallButton = (Button)findViewById(R.id.dict_install_button);
|
||||
mCancelButton = (Button)findViewById(R.id.dict_cancel_button);
|
||||
mDeleteButton = (Button)findViewById(R.id.dict_delete_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 Button getButton(final int status) {
|
||||
switch(status) {
|
||||
case STATUS_INSTALL:
|
||||
return mInstallButton;
|
||||
case STATUS_CANCEL:
|
||||
return mCancelButton;
|
||||
case STATUS_DELETE:
|
||||
return mDeleteButton;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setStatusAndUpdateVisuals(final int status) {
|
||||
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 void setButtonPositionWithoutAnimation(final int status) {
|
||||
// This may be called by setStatus() before the layout has come yet.
|
||||
if (null == mInstallButton) return;
|
||||
final int width = getWidth();
|
||||
// Set to out of the screen if that's not the currently displayed status
|
||||
mInstallButton.setTranslationX(STATUS_INSTALL == status ? 0 : width);
|
||||
mCancelButton.setTranslationX(STATUS_CANCEL == status ? 0 : width);
|
||||
mDeleteButton.setTranslationX(STATUS_DELETE == status ? 0 : width);
|
||||
}
|
||||
|
||||
// The helper method for {@link AnimatorListenerAdapter}.
|
||||
void animateButtonIfStatusIsEqual(final View newButton, final int newStatus) {
|
||||
if (newStatus != mStatus) return;
|
||||
animateButton(newButton, ANIMATION_IN);
|
||||
}
|
||||
|
||||
private void animateButtonPosition(final int oldStatus, final int newStatus) {
|
||||
final View oldButton = getButton(oldStatus);
|
||||
final View newButton = getButton(newStatus);
|
||||
if (null != oldButton && null != newButton) {
|
||||
// Transition between two buttons : animate out, then in
|
||||
animateButton(oldButton, ANIMATION_OUT).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator animation) {
|
||||
animateButtonIfStatusIsEqual(newButton, newStatus);
|
||||
}
|
||||
});
|
||||
} else if (null != oldButton) {
|
||||
animateButton(oldButton, ANIMATION_OUT);
|
||||
} else if (null != newButton) {
|
||||
animateButton(newButton, ANIMATION_IN);
|
||||
}
|
||||
}
|
||||
|
||||
public void setInternalOnClickListener(final OnClickListener listener) {
|
||||
mOnClickListener = listener;
|
||||
if (null != mInstallButton) {
|
||||
// Already laid out : do it now
|
||||
mInstallButton.setOnClickListener(mOnClickListener);
|
||||
mCancelButton.setOnClickListener(mOnClickListener);
|
||||
mDeleteButton.setOnClickListener(mOnClickListener);
|
||||
}
|
||||
}
|
||||
|
||||
private ViewPropertyAnimator animateButton(final View button, final int direction) {
|
||||
final float outerX = getWidth();
|
||||
final float innerX = button.getX() - button.getTranslationX();
|
||||
mInterfaceState.removeFromCache((View)getParent());
|
||||
if (ANIMATION_IN == direction) {
|
||||
button.setClickable(true);
|
||||
return button.animate().translationX(0);
|
||||
}
|
||||
button.setClickable(false);
|
||||
return button.animate().translationX(outerX - innerX);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
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!!.setTranslationX(if (STATUS_INSTALL == status) 0F else width.toFloat())
|
||||
mCancelButton!!.setTranslationX(if (STATUS_CANCEL == status) 0F else width.toFloat())
|
||||
mDeleteButton!!.setTranslationX(if (STATUS_DELETE == status) 0F else width.toFloat())
|
||||
}
|
||||
|
||||
// The helper method for {@link AnimatorListenerAdapter}.
|
||||
fun animateButtonIfStatusIsEqual(newButton: View, newStatus: Int) {
|
||||
if (newStatus != mStatus) return
|
||||
animateButton(newButton, ANIMATION_IN)
|
||||
}
|
||||
|
||||
private fun animateButtonPosition(oldStatus: Int, newStatus: Int) {
|
||||
val oldButton: View? = getButton(oldStatus)
|
||||
val newButton: View? = getButton(newStatus)
|
||||
if (null != oldButton && null != newButton) { // Transition between two buttons : animate out, then in
|
||||
animateButton(oldButton, ANIMATION_OUT).setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
animateButtonIfStatusIsEqual(newButton, newStatus)
|
||||
}
|
||||
})
|
||||
} else oldButton?.let { animateButton(it, ANIMATION_OUT) }
|
||||
?: newButton?.let { animateButton(it, ANIMATION_IN) }
|
||||
}
|
||||
|
||||
fun setInternalOnClickListener(listener: OnClickListener?) {
|
||||
mOnClickListener = listener
|
||||
if (null != mInstallButton) { // Already laid out : do it now
|
||||
mInstallButton!!.setOnClickListener(mOnClickListener)
|
||||
mCancelButton!!.setOnClickListener(mOnClickListener)
|
||||
mDeleteButton!!.setOnClickListener(mOnClickListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateButton(button: View, direction: Int): ViewPropertyAnimator {
|
||||
val outerX = width.toFloat()
|
||||
val innerX = button.x - button.translationX
|
||||
mInterfaceState!!.removeFromCache(parent as View)
|
||||
if (ANIMATION_IN == direction) {
|
||||
button.isClickable = true
|
||||
return button.animate().translationX(0f)
|
||||
}
|
||||
button.isClickable = false
|
||||
return button.animate().translationX(outerX - innerX)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NOT_INITIALIZED = -1
|
||||
const val STATUS_NO_BUTTON = 0
|
||||
const val STATUS_INSTALL = 1
|
||||
const val STATUS_CANCEL = 2
|
||||
const val STATUS_DELETE = 3
|
||||
// Animation directions
|
||||
const val ANIMATION_IN = 1
|
||||
const val ANIMATION_OUT = 2
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public final class CommonPreferences {
|
||||
private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs";
|
||||
|
||||
public static SharedPreferences getCommonPreferences(final Context context) {
|
||||
return context.getSharedPreferences(COMMON_PREFERENCES_NAME, 0);
|
||||
}
|
||||
|
||||
public static void enable(final SharedPreferences pref, final String id) {
|
||||
final SharedPreferences.Editor editor = pref.edit();
|
||||
editor.putBoolean(id, true);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static void disable(final SharedPreferences pref, final String id) {
|
||||
final SharedPreferences.Editor editor = pref.edit();
|
||||
editor.putBoolean(id, false);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
package org.dslul.openboard.inputmethod.dictionarypack
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
object CommonPreferences {
|
||||
private const val COMMON_PREFERENCES_NAME = "LatinImeDictPrefs"
|
||||
fun getCommonPreferences(context: Context): SharedPreferences {
|
||||
return context.getSharedPreferences(COMMON_PREFERENCES_NAME, 0)
|
||||
}
|
||||
|
||||
fun enable(pref: SharedPreferences?, id: String?) {
|
||||
val editor = pref!!.edit()
|
||||
editor.putBoolean(id, true)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
fun disable(pref: SharedPreferences?, id: String?) {
|
||||
val editor = pref!!.edit()
|
||||
editor.putBoolean(id, false)
|
||||
editor.apply()
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public class DictionaryListInterfaceState {
|
||||
static class State {
|
||||
public boolean mOpen = false;
|
||||
public int mStatus = MetadataDbHelper.STATUS_UNKNOWN;
|
||||
}
|
||||
|
||||
private HashMap<String, State> mWordlistToState = new HashMap<>();
|
||||
private ArrayList<View> mViewCache = new ArrayList<>();
|
||||
|
||||
public boolean isOpen(final String wordlistId) {
|
||||
final State state = mWordlistToState.get(wordlistId);
|
||||
if (null == state) return false;
|
||||
return state.mOpen;
|
||||
}
|
||||
|
||||
public int getStatus(final String wordlistId) {
|
||||
final State state = mWordlistToState.get(wordlistId);
|
||||
if (null == state) return MetadataDbHelper.STATUS_UNKNOWN;
|
||||
return state.mStatus;
|
||||
}
|
||||
|
||||
public void setOpen(final String wordlistId, final int status) {
|
||||
final State newState;
|
||||
final State state = mWordlistToState.get(wordlistId);
|
||||
newState = null == state ? new State() : state;
|
||||
newState.mOpen = true;
|
||||
newState.mStatus = status;
|
||||
mWordlistToState.put(wordlistId, newState);
|
||||
}
|
||||
|
||||
public void closeAll() {
|
||||
for (final State state : mWordlistToState.values()) {
|
||||
state.mOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
public View findFirstOrphanedView() {
|
||||
for (final View v : mViewCache) {
|
||||
if (null == v.getParent()) return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public View addToCacheAndReturnView(final View view) {
|
||||
mViewCache.add(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
public void removeFromCache(final View view) {
|
||||
mViewCache.remove(view);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -13,8 +13,7 @@
|
|||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
package org.dslul.openboard.inputmethod.dictionarypack
|
||||
|
||||
/**
|
||||
* A class to group constants for dictionary pack usage.
|
||||
|
@ -23,50 +22,44 @@ package org.dslul.openboard.inputmethod.dictionarypack;
|
|||
* possible, as it's used to separate cleanly the keyboard code from the dictionary pack code; this
|
||||
* is needed in particular to cleanly compile regression tests.
|
||||
*/
|
||||
public class DictionaryPackConstants {
|
||||
object DictionaryPackConstants {
|
||||
/**
|
||||
* The root domain for the dictionary pack, upon which authorities and actions will append
|
||||
* their own distinctive strings.
|
||||
*/
|
||||
private static final String DICTIONARY_DOMAIN = "org.dslul.openboard.inputmethod.dictionarypack.aosp";
|
||||
|
||||
private const val DICTIONARY_DOMAIN = "org.dslul.openboard.inputmethod.dictionarypack.aosp"
|
||||
/**
|
||||
* Authority for the ContentProvider protocol.
|
||||
*/
|
||||
// TODO: find some way to factorize this string with the one in the resources
|
||||
public static final String AUTHORITY = DICTIONARY_DOMAIN;
|
||||
|
||||
// TODO: find some way to factorize this string with the one in the resources
|
||||
const val AUTHORITY = DICTIONARY_DOMAIN
|
||||
/**
|
||||
* The action of the intent for publishing that new dictionary data is available.
|
||||
*/
|
||||
// TODO: make this different across different packages. A suggested course of action is
|
||||
// to use the package name inside this string.
|
||||
// NOTE: The appended string should be uppercase like all other actions, but it's not for
|
||||
// historical reasons.
|
||||
public static final String NEW_DICTIONARY_INTENT_ACTION = DICTIONARY_DOMAIN + ".newdict";
|
||||
|
||||
// TODO: make this different across different packages. A suggested course of action is
|
||||
// to use the package name inside this string.
|
||||
// NOTE: The appended string should be uppercase like all other actions, but it's not for
|
||||
// historical reasons.
|
||||
const val NEW_DICTIONARY_INTENT_ACTION = "$DICTIONARY_DOMAIN.newdict"
|
||||
/**
|
||||
* The action of the intent sent by the dictionary pack to ask for a client to make
|
||||
* itself known. This is used when the settings activity is brought up for a client the
|
||||
* dictionary pack does not know about.
|
||||
*/
|
||||
public static final String UNKNOWN_DICTIONARY_PROVIDER_CLIENT = DICTIONARY_DOMAIN
|
||||
+ ".UNKNOWN_CLIENT";
|
||||
|
||||
const val UNKNOWN_DICTIONARY_PROVIDER_CLIENT = (DICTIONARY_DOMAIN
|
||||
+ ".UNKNOWN_CLIENT")
|
||||
// In the above intents, the name of the string extra that contains the name of the client
|
||||
// we want information about.
|
||||
public static final String DICTIONARY_PROVIDER_CLIENT_EXTRA = "client";
|
||||
|
||||
// we want information about.
|
||||
const val DICTIONARY_PROVIDER_CLIENT_EXTRA = "client"
|
||||
/**
|
||||
* The action of the intent to tell the dictionary provider to update now.
|
||||
*/
|
||||
public static final String UPDATE_NOW_INTENT_ACTION = DICTIONARY_DOMAIN
|
||||
+ ".UPDATE_NOW";
|
||||
|
||||
const val UPDATE_NOW_INTENT_ACTION = (DICTIONARY_DOMAIN
|
||||
+ ".UPDATE_NOW")
|
||||
/**
|
||||
* The intent action to inform the dictionary provider to initialize the db
|
||||
* and update now.
|
||||
*/
|
||||
public static final String INIT_AND_UPDATE_NOW_INTENT_ACTION = DICTIONARY_DOMAIN
|
||||
+ ".INIT_AND_UPDATE_NOW";
|
||||
}
|
||||
const val INIT_AND_UPDATE_NOW_INTENT_ACTION = (DICTIONARY_DOMAIN
|
||||
+ ".INIT_AND_UPDATE_NOW")
|
||||
}
|
|
@ -1,501 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
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.latin.R;
|
||||
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||
import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Provider for dictionaries.
|
||||
*
|
||||
* This class is a ContentProvider exposing all available dictionary data as managed by
|
||||
* the dictionary pack.
|
||||
*/
|
||||
public final class DictionaryProvider extends ContentProvider {
|
||||
private static final String TAG = DictionaryProvider.class.getSimpleName();
|
||||
public static final boolean DEBUG = false;
|
||||
|
||||
public static final Uri CONTENT_URI =
|
||||
Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + DictionaryPackConstants.AUTHORITY);
|
||||
private static final String QUERY_PARAMETER_MAY_PROMPT_USER = "mayPrompt";
|
||||
private static final String QUERY_PARAMETER_TRUE = "true";
|
||||
private static final String QUERY_PARAMETER_DELETE_RESULT = "result";
|
||||
private static final String QUERY_PARAMETER_FAILURE = "failure";
|
||||
public static final String QUERY_PARAMETER_PROTOCOL_VERSION = "protocol";
|
||||
private static final int NO_MATCH = 0;
|
||||
private static final int DICTIONARY_V1_WHOLE_LIST = 1;
|
||||
private static final int DICTIONARY_V1_DICT_INFO = 2;
|
||||
private static final int DICTIONARY_V2_METADATA = 3;
|
||||
private static final int DICTIONARY_V2_WHOLE_LIST = 4;
|
||||
private static final int DICTIONARY_V2_DICT_INFO = 5;
|
||||
private static final int DICTIONARY_V2_DATAFILE = 6;
|
||||
private static final UriMatcher sUriMatcherV1 = new UriMatcher(NO_MATCH);
|
||||
private static final UriMatcher sUriMatcherV2 = new UriMatcher(NO_MATCH);
|
||||
static
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// MIME types for dictionary and dictionary list, as required by ContentProvider contract.
|
||||
public static final String DICT_LIST_MIME_TYPE =
|
||||
"vnd.android.cursor.item/vnd.google.dictionarylist";
|
||||
public static final String DICT_DATAFILE_MIME_TYPE =
|
||||
"vnd.android.cursor.item/vnd.google.dictionary";
|
||||
|
||||
public static final String ID_CATEGORY_SEPARATOR = ":";
|
||||
|
||||
private static final class WordListInfo {
|
||||
public final String mId;
|
||||
public final String mLocale;
|
||||
public final String mRawChecksum;
|
||||
public final int mMatchLevel;
|
||||
public WordListInfo(final String id, final String locale, final String rawChecksum,
|
||||
final int matchLevel) {
|
||||
mId = id;
|
||||
mLocale = locale;
|
||||
mRawChecksum = rawChecksum;
|
||||
mMatchLevel = matchLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 static final class ResourcePathCursor extends AbstractCursor {
|
||||
|
||||
// Column names for the cursor returned by this content provider.
|
||||
static private final String[] columnNames = { MetadataDbHelper.WORDLISTID_COLUMN,
|
||||
MetadataDbHelper.LOCALE_COLUMN, MetadataDbHelper.RAW_CHECKSUM_COLUMN };
|
||||
|
||||
// The list of word lists served by this provider that match the client request.
|
||||
final WordListInfo[] mWordLists;
|
||||
// Note : the cursor also uses mPos, which is defined in AbstractCursor.
|
||||
|
||||
public ResourcePathCursor(final Collection<WordListInfo> wordLists) {
|
||||
// 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.toArray(new WordListInfo[0]);
|
||||
mPos = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getColumnNames() {
|
||||
return columnNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mWordLists.length;
|
||||
}
|
||||
|
||||
@Override public double getDouble(int column) { return 0; }
|
||||
@Override public float getFloat(int column) { return 0; }
|
||||
@Override public int getInt(int column) { return 0; }
|
||||
@Override public short getShort(int column) { return 0; }
|
||||
@Override public long getLong(int column) { return 0; }
|
||||
|
||||
@Override public String getString(final int column) {
|
||||
switch (column) {
|
||||
case 0: return mWordLists[mPos].mId;
|
||||
case 1: return mWordLists[mPos].mLocale;
|
||||
case 2: return mWordLists[mPos].mRawChecksum;
|
||||
default : return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNull(final int column) {
|
||||
if (mPos >= mWordLists.length) return true;
|
||||
return column != 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int matchUri(final Uri uri) {
|
||||
int protocolVersion = 1;
|
||||
final String protocolVersionArg = uri.getQueryParameter(QUERY_PARAMETER_PROTOCOL_VERSION);
|
||||
if ("2".equals(protocolVersionArg)) protocolVersion = 2;
|
||||
switch (protocolVersion) {
|
||||
case 1: return sUriMatcherV1.match(uri);
|
||||
case 2: return sUriMatcherV2.match(uri);
|
||||
default: return NO_MATCH;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getClientId(final Uri uri) {
|
||||
int protocolVersion = 1;
|
||||
final String protocolVersionArg = uri.getQueryParameter(QUERY_PARAMETER_PROTOCOL_VERSION);
|
||||
if ("2".equals(protocolVersionArg)) protocolVersion = 2;
|
||||
switch (protocolVersion) {
|
||||
case 1: return null; // In protocol 1, the client ID is always null.
|
||||
case 2: return uri.getPathSegments().get(0);
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MIME type of the content associated with an Uri
|
||||
*
|
||||
* @see android.content.ContentProvider#getType(android.net.Uri)
|
||||
*
|
||||
* @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
|
||||
public String getType(final Uri uri) {
|
||||
PrivateLog.log("Asked for type of : " + uri);
|
||||
final int match = matchUri(uri);
|
||||
switch (match) {
|
||||
case NO_MATCH: return null;
|
||||
case DICTIONARY_V1_WHOLE_LIST:
|
||||
case DICTIONARY_V1_DICT_INFO:
|
||||
case DICTIONARY_V2_WHOLE_LIST:
|
||||
case DICTIONARY_V2_DICT_INFO: return DICT_LIST_MIME_TYPE;
|
||||
case DICTIONARY_V2_DATAFILE: return DICT_DATAFILE_MIME_TYPE;
|
||||
default: return 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(Uri, String[], String, String[], String)
|
||||
*
|
||||
* @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
|
||||
public Cursor query(final Uri uri, final String[] projection, final String selection,
|
||||
final String[] selectionArgs, final String sortOrder) {
|
||||
DebugLogUtils.l("Uri =", uri);
|
||||
PrivateLog.log("Query : " + uri);
|
||||
final String clientId = getClientId(uri);
|
||||
final int match = matchUri(uri);
|
||||
switch (match) {
|
||||
case DICTIONARY_V1_WHOLE_LIST:
|
||||
case DICTIONARY_V2_WHOLE_LIST:
|
||||
final Cursor c = MetadataDbHelper.queryDictionaries(getContext(), clientId);
|
||||
DebugLogUtils.l("List of dictionaries with count", c.getCount());
|
||||
PrivateLog.log("Returned a list of " + c.getCount() + " items");
|
||||
return c;
|
||||
case 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(getContext(), clientId)) return null;
|
||||
// Fall through
|
||||
case DICTIONARY_V1_DICT_INFO:
|
||||
final String locale = uri.getLastPathSegment();
|
||||
final Collection<WordListInfo> dictFiles =
|
||||
getDictionaryWordListsForLocale(clientId, locale);
|
||||
// TODO: pass clientId to the following function
|
||||
if (null != dictFiles && dictFiles.size() > 0) {
|
||||
PrivateLog.log("Returned " + dictFiles.size() + " files");
|
||||
return new ResourcePathCursor(dictFiles);
|
||||
}
|
||||
PrivateLog.log("No dictionary files for this URL");
|
||||
return new ResourcePathCursor(Collections.<WordListInfo>emptyList());
|
||||
// V2_METADATA and V2_DATAFILE are not supported for query()
|
||||
default:
|
||||
return 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 ContentValues getWordlistMetadataForWordlistId(final String clientId,
|
||||
final String wordlistId) {
|
||||
final Context context = getContext();
|
||||
if (TextUtils.isEmpty(wordlistId)) return null;
|
||||
final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId);
|
||||
return MetadataDbHelper.getInstalledOrDeletingWordListContentValuesByWordListId(
|
||||
db, wordlistId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an asset file for an URI.
|
||||
*
|
||||
* Called by {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)} or
|
||||
* {@link android.content.ContentResolver#openInputStream(Uri)} from a client requesting a
|
||||
* dictionary.
|
||||
* @see android.content.ContentProvider#openAssetFile(Uri, String)
|
||||
*
|
||||
* @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
|
||||
public AssetFileDescriptor openAssetFile(final Uri uri, final String mode) {
|
||||
if (null == mode || !"r".equals(mode)) return null;
|
||||
|
||||
final int 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;
|
||||
}
|
||||
final String wordlistId = uri.getLastPathSegment();
|
||||
final String clientId = getClientId(uri);
|
||||
final ContentValues wordList = getWordlistMetadataForWordlistId(clientId, wordlistId);
|
||||
|
||||
if (null == wordList) return null;
|
||||
|
||||
try {
|
||||
final int 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.
|
||||
final AssetFileDescriptor afd = getContext().getResources().openRawResourceFd(
|
||||
R.raw.empty);
|
||||
return afd;
|
||||
}
|
||||
final String localFilename =
|
||||
wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
|
||||
final File f = getContext().getFileStreamPath(localFilename);
|
||||
final ParcelFileDescriptor pfd =
|
||||
ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
return new AssetFileDescriptor(pfd, 0, pfd.getStatSize());
|
||||
} catch (FileNotFoundException e) {
|
||||
// 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 Collection<WordListInfo> getDictionaryWordListsForLocale(final String clientId,
|
||||
final String locale) {
|
||||
final Context context = getContext();
|
||||
final Cursor results =
|
||||
MetadataDbHelper.queryInstalledOrDeletingOrAvailableDictionaryMetadata(context,
|
||||
clientId);
|
||||
if (null == results) {
|
||||
return Collections.<WordListInfo>emptyList();
|
||||
}
|
||||
try {
|
||||
final HashMap<String, WordListInfo> dicts = new HashMap<>();
|
||||
final int idIndex = results.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN);
|
||||
final int localeIndex = results.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN);
|
||||
final int localFileNameIndex =
|
||||
results.getColumnIndex(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
|
||||
final int rawChecksumIndex =
|
||||
results.getColumnIndex(MetadataDbHelper.RAW_CHECKSUM_COLUMN);
|
||||
final int statusIndex = results.getColumnIndex(MetadataDbHelper.STATUS_COLUMN);
|
||||
if (results.moveToFirst()) {
|
||||
do {
|
||||
final String wordListId = results.getString(idIndex);
|
||||
if (TextUtils.isEmpty(wordListId)) continue;
|
||||
final String[] wordListIdArray =
|
||||
TextUtils.split(wordListId, ID_CATEGORY_SEPARATOR);
|
||||
final String wordListCategory;
|
||||
// This is at the category:manual_id format.
|
||||
wordListCategory = wordListIdArray[0];
|
||||
final String wordListLocale = results.getString(localeIndex);
|
||||
final String wordListLocalFilename = results.getString(localFileNameIndex);
|
||||
final String wordListRawChecksum = results.getString(rawChecksumIndex);
|
||||
final int 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.
|
||||
final int 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.
|
||||
final File f = getContext().getFileStreamPath(wordListLocalFilename);
|
||||
if (!f.isFile()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
final WordListInfo currentBestMatch = dicts.get(wordListCategory);
|
||||
if (null == currentBestMatch
|
||||
|| currentBestMatch.mMatchLevel < matchLevel) {
|
||||
dicts.put(wordListCategory, new WordListInfo(wordListId, wordListLocale,
|
||||
wordListRawChecksum, matchLevel));
|
||||
}
|
||||
} while (results.moveToNext());
|
||||
}
|
||||
return 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(Uri, String, String[])
|
||||
*/
|
||||
@Override
|
||||
public int delete(final Uri uri, final String selection, final String[] selectionArgs)
|
||||
throws UnsupportedOperationException {
|
||||
final int match = matchUri(uri);
|
||||
if (DICTIONARY_V1_DICT_INFO == match || DICTIONARY_V2_DATAFILE == match) {
|
||||
return deleteDataFile(uri);
|
||||
}
|
||||
if (DICTIONARY_V2_METADATA == match) {
|
||||
if (MetadataDbHelper.deleteClient(getContext(), getClientId(uri))) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// Unsupported URI for delete
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int deleteDataFile(final Uri uri) {
|
||||
final String wordlistId = uri.getLastPathSegment();
|
||||
final String clientId = getClientId(uri);
|
||||
final ContentValues wordList = getWordlistMetadataForWordlistId(clientId, wordlistId);
|
||||
if (null == wordList) {
|
||||
return 0;
|
||||
}
|
||||
final int status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||
final int 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
|
||||
*/
|
||||
@Override
|
||||
public Uri insert(final Uri uri, final ContentValues values)
|
||||
throws UnsupportedOperationException {
|
||||
if (null == uri || null == values) return null; // Should never happen but let's be safe
|
||||
PrivateLog.log("Insert, uri = " + uri.toString());
|
||||
final String clientId = getClientId(uri);
|
||||
switch (matchUri(uri)) {
|
||||
case 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(getContext(), clientId, values);
|
||||
break;
|
||||
case DICTIONARY_V2_DICT_INFO:
|
||||
try {
|
||||
final WordListMetadata newDictionaryMetadata =
|
||||
WordListMetadata.createFromContentValues(
|
||||
MetadataDbHelper.completeWithDefaultValues(values));
|
||||
new ActionBatch.MarkPreInstalledAction(clientId, newDictionaryMetadata)
|
||||
.execute(getContext());
|
||||
} catch (final BadFormatException e) {
|
||||
Log.w(TAG, "Not enough information to insert this dictionary " + values, e);
|
||||
}
|
||||
break;
|
||||
case DICTIONARY_V1_WHOLE_LIST:
|
||||
case DICTIONARY_V1_DICT_INFO:
|
||||
PrivateLog.log("Attempt to insert : " + uri);
|
||||
throw new 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(Uri, ContentValues, String, String[])
|
||||
* @see android.content.ContentProvider#insert(Uri, ContentValues)
|
||||
*/
|
||||
@Override
|
||||
public int update(final Uri uri, final ContentValues values, final String selection,
|
||||
final String[] selectionArgs) throws UnsupportedOperationException {
|
||||
PrivateLog.log("Attempt to update : " + uri);
|
||||
throw new UnsupportedOperationException("Updating dictionary words is not supported");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,455 @@
|
|||
/**
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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 Companion.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.Companion.WORDLISTID_COLUMN,
|
||||
MetadataDbHelper.Companion.LOCALE_COLUMN, MetadataDbHelper.Companion.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.Companion.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.Companion.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.Companion.getDb(context, clientId)
|
||||
return MetadataDbHelper.Companion.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.Companion.STATUS_COLUMN)
|
||||
if (MetadataDbHelper.Companion.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.Companion.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.Companion.queryInstalledOrDeletingOrAvailableDictionaryMetadata(context,
|
||||
clientId)
|
||||
?: return emptyList()
|
||||
return try {
|
||||
val dicts = HashMap<String, WordListInfo>()
|
||||
val idIndex = results.getColumnIndex(MetadataDbHelper.Companion.WORDLISTID_COLUMN)
|
||||
val localeIndex = results.getColumnIndex(MetadataDbHelper.Companion.LOCALE_COLUMN)
|
||||
val localFileNameIndex = results.getColumnIndex(MetadataDbHelper.Companion.LOCAL_FILENAME_COLUMN)
|
||||
val rawChecksumIndex = results.getColumnIndex(MetadataDbHelper.Companion.RAW_CHECKSUM_COLUMN)
|
||||
val statusIndex = results.getColumnIndex(MetadataDbHelper.Companion.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.Companion.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.Companion.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.Companion.STATUS_COLUMN)
|
||||
val version = wordList.getAsInteger(MetadataDbHelper.Companion.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 == uri || 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.Companion.updateClientInfo(context, clientId, values)
|
||||
DICTIONARY_V2_DICT_INFO -> try {
|
||||
val newDictionaryMetadata: WordListMetadata = WordListMetadata.Companion.createFromContentValues(
|
||||
MetadataDbHelper.Companion.completeWithDefaultValues(values))
|
||||
MarkPreInstalledAction(clientId, newDictionaryMetadata)
|
||||
.execute(context)
|
||||
} catch (e: BadFormatException) {
|
||||
Log.w(TAG, "Not enough information to insert this dictionary $values", e)
|
||||
}
|
||||
DICTIONARY_V1_WHOLE_LIST, DICTIONARY_V1_DICT_INFO -> {
|
||||
PrivateLog.log("Attempt to insert : $uri")
|
||||
throw UnsupportedOperationException(
|
||||
"Insertion in the dictionary is not supported in this version")
|
||||
}
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
/**
|
||||
* Updating data is not supported, and will throw an exception.
|
||||
* @see android.content.ContentProvider.update
|
||||
* @see android.content.ContentProvider.insert
|
||||
*/
|
||||
@Throws(UnsupportedOperationException::class)
|
||||
override fun update(uri: Uri, values: ContentValues?, selection: String?,
|
||||
selectionArgs: Array<String>?): Int {
|
||||
PrivateLog.log("Attempt to update : $uri")
|
||||
throw UnsupportedOperationException("Updating dictionary words is not supported")
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
|
||||
import org.dslul.openboard.inputmethod.latin.utils.FragmentUtils;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceActivity;
|
||||
|
||||
/**
|
||||
* Preference screen.
|
||||
*/
|
||||
public final class DictionarySettingsActivity extends PreferenceActivity {
|
||||
private static final String DEFAULT_FRAGMENT = DictionarySettingsFragment.class.getName();
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent getIntent() {
|
||||
final Intent modIntent = new Intent(super.getIntent());
|
||||
modIntent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
|
||||
modIntent.putExtra(EXTRA_NO_HEADERS, true);
|
||||
// Important note : the original intent should contain a String extra with the key
|
||||
// DictionarySettingsFragment.DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT so that the
|
||||
// fragment can know who the client is.
|
||||
return modIntent;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
@Override
|
||||
public boolean isValidFragment(String fragmentName) {
|
||||
return FragmentUtils.isValidFragment(fragmentName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
package org.dslul.openboard.inputmethod.dictionarypack
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceActivity
|
||||
import org.dslul.openboard.inputmethod.latin.utils.FragmentUtils
|
||||
|
||||
/**
|
||||
* Preference screen.
|
||||
*/
|
||||
class DictionarySettingsActivity : PreferenceActivity() {
|
||||
|
||||
override fun getIntent(): Intent {
|
||||
val modIntent = Intent(super.getIntent())
|
||||
modIntent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT)
|
||||
modIntent.putExtra(EXTRA_NO_HEADERS, true)
|
||||
// Important note : the original intent should contain a String extra with the key
|
||||
// DictionarySettingsFragment.DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT so that the
|
||||
// fragment can know who the client is.
|
||||
return modIntent
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public override fun isValidFragment(fragmentName: String): Boolean {
|
||||
return FragmentUtils.isValidFragment(fragmentName)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_FRAGMENT = DictionarySettingsFragment::class.java.name
|
||||
}
|
||||
}
|
|
@ -1,282 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
|
||||
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.database.Cursor;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceGroup;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AnimationUtils;
|
||||
|
||||
import org.dslul.openboard.inputmethod.latin.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Preference screen.
|
||||
*/
|
||||
public final class DictionarySettingsFragment extends PreferenceFragment {
|
||||
private static final String TAG = DictionarySettingsFragment.class.getSimpleName();
|
||||
|
||||
static final private String DICT_LIST_ID = "list";
|
||||
static final public String DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT = "clientId";
|
||||
|
||||
static final private int MENU_UPDATE_NOW = Menu.FIRST;
|
||||
|
||||
private View mLoadingView;
|
||||
private String mClientId;
|
||||
//private ConnectivityManager mConnectivityManager;
|
||||
private MenuItem mUpdateNowMenu;
|
||||
private boolean mChangedSettings;
|
||||
private DictionaryListInterfaceState mDictionaryListInterfaceState =
|
||||
new DictionaryListInterfaceState();
|
||||
// never null
|
||||
private TreeMap<String, WordListPreference> mCurrentPreferenceMap = new TreeMap<>();
|
||||
|
||||
/**
|
||||
* Empty constructor for fragment generation.
|
||||
*/
|
||||
public DictionarySettingsFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
final View v = inflater.inflate(R.layout.loading_page, container, true);
|
||||
mLoadingView = v.findViewById(R.id.loading_container);
|
||||
return super.onCreateView(inflater, container, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(final Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
final Activity activity = getActivity();
|
||||
mClientId = activity.getIntent().getStringExtra(DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT);
|
||||
/*mConnectivityManager =
|
||||
(ConnectivityManager)activity.getSystemService(Context.CONNECTIVITY_SERVICE);*/
|
||||
addPreferencesFromResource(R.xml.dictionary_settings);
|
||||
refreshInterface();
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
mChangedSettings = false;
|
||||
final Activity activity = getActivity();
|
||||
final IntentFilter filter = new IntentFilter();
|
||||
|
||||
new Thread("onResume") {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!MetadataDbHelper.isClientKnown(activity, mClientId)) {
|
||||
Log.i(TAG, "Unknown dictionary pack client: " + mClientId
|
||||
+ ". Requesting info.");
|
||||
final Intent unknownClientBroadcast =
|
||||
new Intent(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT);
|
||||
unknownClientBroadcast.putExtra(
|
||||
DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA, mClientId);
|
||||
activity.sendBroadcast(unknownClientBroadcast);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
final Activity activity = getActivity();
|
||||
if (mChangedSettings) {
|
||||
final Intent newDictBroadcast =
|
||||
new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
|
||||
activity.sendBroadcast(newDictBroadcast);
|
||||
mChangedSettings = false;
|
||||
}
|
||||
}
|
||||
|
||||
private WordListPreference findWordListPreference(final String id) {
|
||||
final PreferenceGroup prefScreen = getPreferenceScreen();
|
||||
if (null == prefScreen) {
|
||||
Log.e(TAG, "Could not find the preference group");
|
||||
return null;
|
||||
}
|
||||
for (int i = prefScreen.getPreferenceCount() - 1; i >= 0; --i) {
|
||||
final Preference pref = prefScreen.getPreference(i);
|
||||
if (pref instanceof WordListPreference) {
|
||||
final WordListPreference wlPref = (WordListPreference)pref;
|
||||
if (id.equals(wlPref.mWordlistId)) {
|
||||
return wlPref;
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.e(TAG, "Could not find the preference for a word list id " + id);
|
||||
return null;
|
||||
}
|
||||
|
||||
void refreshInterface() {
|
||||
final Activity activity = getActivity();
|
||||
if (null == activity) return;
|
||||
final PreferenceGroup prefScreen = getPreferenceScreen();
|
||||
final Collection<? extends Preference> prefList =
|
||||
createInstalledDictSettingsCollection(mClientId);
|
||||
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// TODO: display this somewhere
|
||||
// if (0 != lastUpdate) mUpdateNowPreference.setSummary(updateNowSummary);
|
||||
removeAnyDictSettings(prefScreen);
|
||||
int i = 0;
|
||||
for (Preference preference : prefList) {
|
||||
preference.setOrder(i++);
|
||||
prefScreen.addPreference(preference);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Preference createErrorMessage(final Activity activity, final int messageResource) {
|
||||
final Preference message = new Preference(activity);
|
||||
message.setTitle(messageResource);
|
||||
message.setEnabled(false);
|
||||
return message;
|
||||
}
|
||||
|
||||
static void removeAnyDictSettings(final PreferenceGroup prefGroup) {
|
||||
for (int i = prefGroup.getPreferenceCount() - 1; i >= 0; --i) {
|
||||
prefGroup.removePreference(prefGroup.getPreference(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Collection<? extends Preference> createInstalledDictSettingsCollection(
|
||||
final String clientId) {
|
||||
// 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
|
||||
final Uri contentUri = new 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.QUERY_PARAMETER_PROTOCOL_VERSION, "2")
|
||||
.build();
|
||||
final Activity activity = getActivity();
|
||||
final Cursor cursor = (null == activity) ? null
|
||||
: activity.getContentResolver().query(contentUri, null, null, null, null);
|
||||
|
||||
if (null == cursor) {
|
||||
final ArrayList<Preference> result = new ArrayList<>();
|
||||
result.add(createErrorMessage(activity, R.string.cannot_connect_to_dict_service));
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
if (!cursor.moveToFirst()) {
|
||||
final ArrayList<Preference> result = new ArrayList<>();
|
||||
result.add(createErrorMessage(activity, R.string.no_dictionaries_available));
|
||||
return result;
|
||||
}
|
||||
final String systemLocaleString = Locale.getDefault().toString();
|
||||
final TreeMap<String, WordListPreference> prefMap = new TreeMap<>();
|
||||
final int idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN);
|
||||
final int versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN);
|
||||
final int localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN);
|
||||
final int descriptionIndex = cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN);
|
||||
final int statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN);
|
||||
final int filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN);
|
||||
do {
|
||||
final String wordlistId = cursor.getString(idIndex);
|
||||
final int version = cursor.getInt(versionIndex);
|
||||
final String localeString = cursor.getString(localeIndex);
|
||||
final Locale locale = new Locale(localeString);
|
||||
final String description = cursor.getString(descriptionIndex);
|
||||
final int status = cursor.getInt(statusIndex);
|
||||
final int matchLevel = LocaleUtils.getMatchLevel(systemLocaleString, localeString);
|
||||
final String matchLevelString = LocaleUtils.getMatchLevelSortedString(matchLevel);
|
||||
final int filesize = cursor.getInt(filesizeIndex);
|
||||
// The key is sorted in lexicographic order, according to the match level, then
|
||||
// the description.
|
||||
final String key = matchLevelString + "." + description + "." + wordlistId;
|
||||
final WordListPreference existingPref = prefMap.get(key);
|
||||
if (null == existingPref || existingPref.hasPriorityOver(status)) {
|
||||
final WordListPreference oldPreference = mCurrentPreferenceMap.get(key);
|
||||
final WordListPreference pref;
|
||||
if (null != oldPreference
|
||||
&& oldPreference.mVersion == version
|
||||
&& oldPreference.hasStatus(status)
|
||||
&& oldPreference.mLocale.equals(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.
|
||||
pref = 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.
|
||||
pref = new WordListPreference(activity, mDictionaryListInterfaceState,
|
||||
mClientId, wordlistId, version, locale, description, status,
|
||||
filesize);
|
||||
}
|
||||
prefMap.put(key, pref);
|
||||
}
|
||||
} while (cursor.moveToNext());
|
||||
mCurrentPreferenceMap = prefMap;
|
||||
return prefMap.values();
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
/**
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.Companion.WORDLISTID_COLUMN)
|
||||
val versionIndex = cursor.getColumnIndex(MetadataDbHelper.Companion.VERSION_COLUMN)
|
||||
val localeIndex = cursor.getColumnIndex(MetadataDbHelper.Companion.LOCALE_COLUMN)
|
||||
val descriptionIndex = cursor.getColumnIndex(MetadataDbHelper.Companion.DESCRIPTION_COLUMN)
|
||||
val statusIndex = cursor.getColumnIndex(MetadataDbHelper.Companion.STATUS_COLUMN)
|
||||
val filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.Companion.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,23 +13,16 @@
|
|||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package org.dslul.openboard.inputmethod.dictionarypack
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* A very simple problem reporter.
|
||||
*/
|
||||
final class LogProblemReporter implements ProblemReporter {
|
||||
private final String TAG;
|
||||
|
||||
public LogProblemReporter(final String tag) {
|
||||
TAG = tag;
|
||||
internal class LogProblemReporter(private val TAG: String) : ProblemReporter {
|
||||
override fun report(e: Exception?) {
|
||||
Log.e(TAG, "Reporting problem", e)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void report(final Exception e) {
|
||||
Log.e(TAG, "Reporting problem", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
public final class MD5Calculator {
|
||||
private MD5Calculator() {} // This helper class is not instantiable
|
||||
|
||||
public static String checksum(final InputStream in) throws IOException {
|
||||
// This code from the Android documentation for MessageDigest. Nearly verbatim.
|
||||
MessageDigest digester;
|
||||
try {
|
||||
digester = MessageDigest.getInstance("MD5");
|
||||
} catch (java.security.NoSuchAlgorithmException e) {
|
||||
return null; // Platform does not support MD5 : can't check, so return null
|
||||
}
|
||||
final byte[] bytes = new byte[8192];
|
||||
int byteCount;
|
||||
while ((byteCount = in.read(bytes)) > 0) {
|
||||
digester.update(bytes, 0, byteCount);
|
||||
}
|
||||
final byte[] digest = digester.digest();
|
||||
final StringBuilder s = new StringBuilder();
|
||||
for (int i = 0; i < digest.length; ++i) {
|
||||
s.append(String.format("%1$02x", digest[i]));
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
package org.dslul.openboard.inputmethod.dictionarypack
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
object MD5Calculator {
|
||||
@Throws(IOException::class)
|
||||
fun checksum(`in`: InputStream): String? { // This code from the Android documentation for MessageDigest. Nearly verbatim.
|
||||
val digester: MessageDigest
|
||||
digester = try {
|
||||
MessageDigest.getInstance("MD5")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
return null // Platform does not support MD5 : can't check, so return null
|
||||
}
|
||||
val bytes = ByteArray(8192)
|
||||
var byteCount: Int
|
||||
while (`in`.read(bytes).also { byteCount = it } > 0) {
|
||||
digester.update(bytes, 0, byteCount)
|
||||
}
|
||||
val digest = digester.digest()
|
||||
val s = StringBuilder()
|
||||
for (i in digest.indices) {
|
||||
s.append(String.format("%1$02x", digest[i]))
|
||||
}
|
||||
return s.toString()
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,952 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
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) ?: return
|
||||
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 if (0 == defaultDb.delete(CLIENT_TABLE_NAME,
|
||||
"$CLIENT_CLIENT_ID_COLUMN = ?", arrayOf(clientId))) {
|
||||
false
|
||||
} else true
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates information relative to a specific client.
|
||||
*
|
||||
* Updatable information includes the metadata URI and the additional ID column. It may be
|
||||
* expanded in the future.
|
||||
* The passed values must include a client ID in the key CLIENT_CLIENT_ID_COLUMN, and it must
|
||||
* be equal to the string passed as an argument for clientId. It may not be empty.
|
||||
* The passed values must also include a non-null metadata URI in the
|
||||
* CLIENT_METADATA_URI_COLUMN column, as well as a non-null additional ID in the
|
||||
* CLIENT_METADATA_ADDITIONAL_ID_COLUMN. Both these strings may be empty.
|
||||
* If any of the above is not complied with, this function returns without updating data.
|
||||
*
|
||||
* @param context the context, to open the database
|
||||
* @param clientId the ID of the client to update
|
||||
* @param values the values to update. Must conform to the protocol (see above)
|
||||
*/
|
||||
fun updateClientInfo(context: Context?, clientId: String?,
|
||||
values: ContentValues) { // Sanity check the content values
|
||||
val valuesClientId = values.getAsString(CLIENT_CLIENT_ID_COLUMN)
|
||||
val valuesMetadataUri = values.getAsString(CLIENT_METADATA_URI_COLUMN)
|
||||
val valuesMetadataAdditionalId = values.getAsString(CLIENT_METADATA_ADDITIONAL_ID_COLUMN)
|
||||
// Empty string is a valid client ID, but external apps may not configure it, so disallow
|
||||
// both null and empty string.
|
||||
// Empty string is a valid metadata URI if the client does not want updates, so allow
|
||||
// empty string but disallow null.
|
||||
// Empty string is a valid additional ID so allow empty string but disallow null.
|
||||
if (TextUtils.isEmpty(valuesClientId) || null == valuesMetadataUri || null == valuesMetadataAdditionalId) { // We need all these columns to be filled in
|
||||
DebugLogUtils.l("Missing parameter for updateClientInfo")
|
||||
return
|
||||
}
|
||||
if (clientId != valuesClientId) { // Mismatch! The client violates the protocol.
|
||||
DebugLogUtils.l("Received an updateClientInfo request for ", clientId,
|
||||
" but the values " + "contain a different ID : ", valuesClientId)
|
||||
return
|
||||
}
|
||||
// Default value for a pending ID is NOT_AN_ID
|
||||
val defaultDb = getDb(context, "")
|
||||
if (-1L == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) {
|
||||
defaultDb.update(CLIENT_TABLE_NAME, values,
|
||||
"$CLIENT_CLIENT_ID_COLUMN = ?", arrayOf(clientId))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of existing client IDs.
|
||||
* @param context the context to open the database
|
||||
* @return a cursor containing only one column, and one client ID per line.
|
||||
*/
|
||||
fun queryClientIds(context: Context?): Cursor {
|
||||
return getDb(context, null).query(CLIENT_TABLE_NAME, arrayOf(CLIENT_CLIENT_ID_COLUMN), null, null, null, null, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a downloading entry as having successfully downloaded and being installed.
|
||||
*
|
||||
* The metadata database contains information about ongoing processes, typically ongoing
|
||||
* downloads. This marks such an entry as having finished and having installed successfully,
|
||||
* so it becomes INSTALLED.
|
||||
*
|
||||
* @param db the metadata database.
|
||||
* @param r content values about the entry to mark as processed.
|
||||
*/
|
||||
fun markEntryAsFinishedDownloadingAndInstalled(db: SQLiteDatabase,
|
||||
r: ContentValues) {
|
||||
when (r.getAsInteger(TYPE_COLUMN)) {
|
||||
TYPE_BULK -> {
|
||||
DebugLogUtils.l("Ended processing a wordlist")
|
||||
// Updating a bulk word list is a three-step operation:
|
||||
// - Add the new entry to the table
|
||||
// - Remove the old entry from the table
|
||||
// - Erase the old file
|
||||
// We start by gathering the names of the files we should delete.
|
||||
val filenames: MutableList<String> = LinkedList()
|
||||
val c = db.query(METADATA_TABLE_NAME, arrayOf(LOCAL_FILENAME_COLUMN),
|
||||
LOCALE_COLUMN + " = ? AND " +
|
||||
WORDLISTID_COLUMN + " = ? AND " + STATUS_COLUMN + " = ?", arrayOf(r.getAsString(LOCALE_COLUMN),
|
||||
r.getAsString(WORDLISTID_COLUMN),
|
||||
Integer.toString(STATUS_INSTALLED)),
|
||||
null, null, null)
|
||||
try {
|
||||
if (c.moveToFirst()) { // There should never be more than one file, but if there are, it's a bug
|
||||
// and we should remove them all. I think it might happen if the power of
|
||||
// the phone is suddenly cut during an update.
|
||||
val filenameIndex = c.getColumnIndex(LOCAL_FILENAME_COLUMN)
|
||||
do {
|
||||
DebugLogUtils.l("Setting for removal", c.getString(filenameIndex))
|
||||
filenames.add(c.getString(filenameIndex))
|
||||
} while (c.moveToNext())
|
||||
}
|
||||
} finally {
|
||||
c.close()
|
||||
}
|
||||
r.put(STATUS_COLUMN, STATUS_INSTALLED)
|
||||
db.beginTransactionNonExclusive()
|
||||
// Delete all old entries. There should never be any stalled entries, but if
|
||||
// there are, this deletes them.
|
||||
db.delete(METADATA_TABLE_NAME,
|
||||
"$WORDLISTID_COLUMN = ?", arrayOf(r.getAsString(WORDLISTID_COLUMN)))
|
||||
db.insert(METADATA_TABLE_NAME, null, r)
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
for (filename in filenames) {
|
||||
try {
|
||||
val f = File(filename)
|
||||
f.delete()
|
||||
} catch (e: SecurityException) { // No permissions to delete. Um. Can't do anything.
|
||||
} // I don't think anything else can be thrown
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a downloading entry from the database.
|
||||
*
|
||||
* This is invoked when a download fails. Either we tried to download, but
|
||||
* we received a permanent failure and we should remove it, or we got manually
|
||||
* cancelled and we should leave it at that.
|
||||
*
|
||||
* @param db the metadata database.
|
||||
* @param id the DownloadManager id of the file.
|
||||
*/
|
||||
fun deleteDownloadingEntry(db: SQLiteDatabase, id: Long) {
|
||||
db.delete(METADATA_TABLE_NAME, "$PENDINGID_COLUMN = ? AND $STATUS_COLUMN = ?", arrayOf(java.lang.Long.toString(id), Integer.toString(STATUS_DOWNLOADING)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcefully removes an entry from the database.
|
||||
*
|
||||
* This is invoked when a file is broken. The file has been downloaded, but Android
|
||||
* Keyboard is telling us it could not open it.
|
||||
*
|
||||
* @param db the metadata database.
|
||||
* @param id the id of the word list.
|
||||
* @param version the version of the word list.
|
||||
*/
|
||||
fun deleteEntry(db: SQLiteDatabase, id: String, version: Int) {
|
||||
db.delete(METADATA_TABLE_NAME, "$WORDLISTID_COLUMN = ? AND $VERSION_COLUMN = ?", arrayOf(id, Integer.toString(version)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method that sets the current status of an entry of the database.
|
||||
*
|
||||
* @param db the metadata database.
|
||||
* @param id the id of the word list.
|
||||
* @param version the version of the word list.
|
||||
* @param status the status to set the word list to.
|
||||
* @param downloadId an optional download id to write, or NOT_A_DOWNLOAD_ID
|
||||
*/
|
||||
private fun markEntryAs(db: SQLiteDatabase, id: String?,
|
||||
version: Int, status: Int, downloadId: Long) {
|
||||
val values = getContentValuesByWordListId(db, id, version)
|
||||
values!!.put(STATUS_COLUMN, status)
|
||||
if (NOT_A_DOWNLOAD_ID != downloadId) {
|
||||
values.put(PENDINGID_COLUMN, downloadId)
|
||||
}
|
||||
db.update(METADATA_TABLE_NAME, values,
|
||||
"$WORDLISTID_COLUMN = ? AND $VERSION_COLUMN = ?", arrayOf(id, Integer.toString(version)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the status column for the wordlist with this id as enabled. Typically this
|
||||
* means the word list is currently disabled and we want to set its status to INSTALLED.
|
||||
*
|
||||
* @param db the metadata database.
|
||||
* @param id the id of the word list.
|
||||
* @param version the version of the word list.
|
||||
*/
|
||||
fun markEntryAsEnabled(db: SQLiteDatabase, id: String?,
|
||||
version: Int) {
|
||||
markEntryAs(db, id, version, STATUS_INSTALLED, NOT_A_DOWNLOAD_ID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the status column for the wordlist with this id as disabled. Typically this
|
||||
* means the word list is currently installed and we want to set its status to DISABLED.
|
||||
*
|
||||
* @param db the metadata database.
|
||||
* @param id the id of the word list.
|
||||
* @param version the version of the word list.
|
||||
*/
|
||||
fun markEntryAsDisabled(db: SQLiteDatabase, id: String?,
|
||||
version: Int) {
|
||||
markEntryAs(db, id, version, STATUS_DISABLED, NOT_A_DOWNLOAD_ID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the status column for the wordlist with this id as available. This happens for
|
||||
* example when a word list has been deleted but can be downloaded again.
|
||||
*
|
||||
* @param db the metadata database.
|
||||
* @param id the id of the word list.
|
||||
* @param version the version of the word list.
|
||||
*/
|
||||
fun markEntryAsAvailable(db: SQLiteDatabase, id: String?,
|
||||
version: Int) {
|
||||
markEntryAs(db, id, version, STATUS_AVAILABLE, NOT_A_DOWNLOAD_ID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the designated word list as downloadable, alongside with its download id.
|
||||
*
|
||||
* @param db the metadata database.
|
||||
* @param id the id of the word list.
|
||||
* @param version the version of the word list.
|
||||
* @param downloadId the download id.
|
||||
*/
|
||||
fun markEntryAsDownloading(db: SQLiteDatabase, id: String?,
|
||||
version: Int, downloadId: Long) {
|
||||
markEntryAs(db, id, version, STATUS_DOWNLOADING, downloadId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the designated word list as deleting.
|
||||
*
|
||||
* @param db the metadata database.
|
||||
* @param id the id of the word list.
|
||||
* @param version the version of the word list.
|
||||
*/
|
||||
fun markEntryAsDeleting(db: SQLiteDatabase, id: String?,
|
||||
version: Int) {
|
||||
markEntryAs(db, id, version, STATUS_DELETING, NOT_A_DOWNLOAD_ID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks retry counts and marks the word list as retrying if retry is possible.
|
||||
*
|
||||
* @param db the metadata database.
|
||||
* @param id the id of the word list.
|
||||
* @param version the version of the word list.
|
||||
* @return `true` if the retry is possible.
|
||||
*/
|
||||
fun maybeMarkEntryAsRetrying(db: SQLiteDatabase, id: String?,
|
||||
version: Int): Boolean {
|
||||
val values = getContentValuesByWordListId(db, id, version)
|
||||
val retryCount = values!!.getAsInteger(RETRY_COUNT_COLUMN)
|
||||
if (retryCount > 1) {
|
||||
values.put(STATUS_COLUMN, STATUS_RETRYING)
|
||||
values.put(RETRY_COUNT_COLUMN, retryCount - 1)
|
||||
db.update(METADATA_TABLE_NAME, values,
|
||||
"$WORDLISTID_COLUMN = ? AND $VERSION_COLUMN = ?", arrayOf(id, Integer.toString(version)))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,172 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
|
||||
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.Collections;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class to easy up manipulation of dictionary pack metadata.
|
||||
*/
|
||||
public class MetadataHandler {
|
||||
|
||||
public static final String TAG = MetadataHandler.class.getSimpleName();
|
||||
|
||||
// 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.
|
||||
public static final String 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 static List<WordListMetadata> makeMetadataObject(final Cursor results) {
|
||||
final ArrayList<WordListMetadata> buildingMetadata = new ArrayList<>();
|
||||
if (null != results && results.moveToFirst()) {
|
||||
final int localeColumn = results.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN);
|
||||
final int typeColumn = results.getColumnIndex(MetadataDbHelper.TYPE_COLUMN);
|
||||
final int descriptionColumn =
|
||||
results.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN);
|
||||
final int idIndex = results.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN);
|
||||
final int updateIndex = results.getColumnIndex(MetadataDbHelper.DATE_COLUMN);
|
||||
final int fileSizeIndex = results.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN);
|
||||
final int rawChecksumIndex =
|
||||
results.getColumnIndex(MetadataDbHelper.RAW_CHECKSUM_COLUMN);
|
||||
final int checksumIndex = results.getColumnIndex(MetadataDbHelper.CHECKSUM_COLUMN);
|
||||
final int retryCountIndex = results.getColumnIndex(MetadataDbHelper.RETRY_COUNT_COLUMN);
|
||||
final int localFilenameIndex =
|
||||
results.getColumnIndex(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
|
||||
final int remoteFilenameIndex =
|
||||
results.getColumnIndex(MetadataDbHelper.REMOTE_FILENAME_COLUMN);
|
||||
final int versionIndex = results.getColumnIndex(MetadataDbHelper.VERSION_COLUMN);
|
||||
final int formatVersionIndex =
|
||||
results.getColumnIndex(MetadataDbHelper.FORMATVERSION_COLUMN);
|
||||
do {
|
||||
buildingMetadata.add(new 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.
|
||||
*/
|
||||
public static List<WordListMetadata> getCurrentMetadata(final Context context,
|
||||
final String clientId) {
|
||||
// If clientId is null, we get a cursor on the default database (see
|
||||
// MetadataDbHelper#getInstance() for more on this)
|
||||
final Cursor results = MetadataDbHelper.queryCurrentMetadata(context, clientId);
|
||||
// If null, we should return makeMetadataObject(null), so we go through.
|
||||
try {
|
||||
return makeMetadataObject(results);
|
||||
} finally {
|
||||
if (null != results) {
|
||||
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
|
||||
*/
|
||||
public static WordListMetadata getCurrentMetadataForWordList(final Context context,
|
||||
final String clientId, final String wordListId, final int version) {
|
||||
final ContentValues contentValues = MetadataDbHelper.getContentValuesByWordListId(
|
||||
MetadataDbHelper.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.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
|
||||
*/
|
||||
public static List<WordListMetadata> readMetadata(final InputStreamReader input)
|
||||
throws IOException, BadFormatException {
|
||||
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.
|
||||
*/
|
||||
public static WordListMetadata findWordListById(final List<WordListMetadata> metadata,
|
||||
final String id) {
|
||||
WordListMetadata bestWordList = null;
|
||||
int bestFormatVersion = Integer.MIN_VALUE; // To be sure we can't be inadvertently smaller
|
||||
for (WordListMetadata wordList : metadata) {
|
||||
if (id.equals(wordList.mId)
|
||||
&& wordList.mFormatVersion > bestFormatVersion) {
|
||||
bestWordList = wordList;
|
||||
bestFormatVersion = wordList.mFormatVersion;
|
||||
}
|
||||
}
|
||||
// If we didn't find any match we'll return null.
|
||||
return bestWordList;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
package org.dslul.openboard.inputmethod.dictionarypack
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.util.Log
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Helper class to easy up manipulation of dictionary pack metadata.
|
||||
*/
|
||||
object MetadataHandler {
|
||||
val TAG = MetadataHandler::class.java.simpleName
|
||||
// The canonical file name for metadata. This is not the name of a real file on the
|
||||
// device, but a symbolic name used in the database and in metadata handling. It is never
|
||||
// tested against, only used for human-readability as the file name for the metadata.
|
||||
const val METADATA_FILENAME = "metadata.json"
|
||||
|
||||
/**
|
||||
* Reads the data from the cursor and store it in metadata objects.
|
||||
* @param results the cursor to read data from.
|
||||
* @return the constructed list of wordlist metadata.
|
||||
*/
|
||||
private fun makeMetadataObject(results: Cursor?): List<WordListMetadata> {
|
||||
val buildingMetadata = ArrayList<WordListMetadata>()
|
||||
if (null != results && results.moveToFirst()) {
|
||||
val localeColumn = results.getColumnIndex(MetadataDbHelper.Companion.LOCALE_COLUMN)
|
||||
val typeColumn = results.getColumnIndex(MetadataDbHelper.Companion.TYPE_COLUMN)
|
||||
val descriptionColumn = results.getColumnIndex(MetadataDbHelper.Companion.DESCRIPTION_COLUMN)
|
||||
val idIndex = results.getColumnIndex(MetadataDbHelper.Companion.WORDLISTID_COLUMN)
|
||||
val updateIndex = results.getColumnIndex(MetadataDbHelper.Companion.DATE_COLUMN)
|
||||
val fileSizeIndex = results.getColumnIndex(MetadataDbHelper.Companion.FILESIZE_COLUMN)
|
||||
val rawChecksumIndex = results.getColumnIndex(MetadataDbHelper.Companion.RAW_CHECKSUM_COLUMN)
|
||||
val checksumIndex = results.getColumnIndex(MetadataDbHelper.Companion.CHECKSUM_COLUMN)
|
||||
val retryCountIndex = results.getColumnIndex(MetadataDbHelper.Companion.RETRY_COUNT_COLUMN)
|
||||
val localFilenameIndex = results.getColumnIndex(MetadataDbHelper.Companion.LOCAL_FILENAME_COLUMN)
|
||||
val remoteFilenameIndex = results.getColumnIndex(MetadataDbHelper.Companion.REMOTE_FILENAME_COLUMN)
|
||||
val versionIndex = results.getColumnIndex(MetadataDbHelper.Companion.VERSION_COLUMN)
|
||||
val formatVersionIndex = results.getColumnIndex(MetadataDbHelper.Companion.FORMATVERSION_COLUMN)
|
||||
do {
|
||||
buildingMetadata.add(WordListMetadata(results.getString(idIndex),
|
||||
results.getInt(typeColumn),
|
||||
results.getString(descriptionColumn),
|
||||
results.getLong(updateIndex),
|
||||
results.getLong(fileSizeIndex),
|
||||
results.getString(rawChecksumIndex),
|
||||
results.getString(checksumIndex),
|
||||
results.getInt(retryCountIndex),
|
||||
results.getString(localFilenameIndex),
|
||||
results.getString(remoteFilenameIndex),
|
||||
results.getInt(versionIndex),
|
||||
results.getInt(formatVersionIndex),
|
||||
0, results.getString(localeColumn)))
|
||||
} while (results.moveToNext())
|
||||
}
|
||||
return Collections.unmodifiableList(buildingMetadata)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the whole metadata, for installed and not installed dictionaries.
|
||||
* @param context The context to open files over.
|
||||
* @param clientId the client id for retrieving the database. null for default (deprecated)
|
||||
* @return The current metadata.
|
||||
*/
|
||||
fun getCurrentMetadata(context: Context?,
|
||||
clientId: String?): List<WordListMetadata> { // If clientId is null, we get a cursor on the default database (see
|
||||
// MetadataDbHelper#getInstance() for more on this)
|
||||
val results: Cursor = MetadataDbHelper.Companion.queryCurrentMetadata(context, clientId)
|
||||
// If null, we should return makeMetadataObject(null), so we go through.
|
||||
return try {
|
||||
makeMetadataObject(results)
|
||||
} finally {
|
||||
results?.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the metadata, for a specific dictionary.
|
||||
*
|
||||
* @param context The context to open files over.
|
||||
* @param clientId the client id for retrieving the database. null for default (deprecated).
|
||||
* @param wordListId the word list ID.
|
||||
* @param version the word list version.
|
||||
* @return the current metaData
|
||||
*/
|
||||
fun getCurrentMetadataForWordList(context: Context?,
|
||||
clientId: String?, wordListId: String?, version: Int): WordListMetadata? {
|
||||
val contentValues: ContentValues = MetadataDbHelper.Companion.getContentValuesByWordListId(
|
||||
MetadataDbHelper.Companion.getDb(context, clientId), wordListId, version)!!
|
||||
if (contentValues == null) { // TODO: Figure out why this would happen.
|
||||
// Check if this happens when the metadata gets updated in the background.
|
||||
Log.e(TAG, String.format("Unable to find the current metadata for wordlist "
|
||||
+ "(clientId=%s, wordListId=%s, version=%d) on the database",
|
||||
clientId, wordListId, version))
|
||||
return null
|
||||
}
|
||||
return WordListMetadata.Companion.createFromContentValues(contentValues)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read metadata from a stream.
|
||||
* @param input The stream to read from.
|
||||
* @return The read metadata.
|
||||
* @throws IOException if the input stream cannot be read
|
||||
* @throws BadFormatException if the stream is not in a known format
|
||||
*/
|
||||
@Throws(IOException::class, BadFormatException::class)
|
||||
fun readMetadata(input: InputStreamReader?): List<WordListMetadata?>? {
|
||||
return MetadataParser.parseMetadata(input)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a single WordListMetadata inside a whole metadata chunk.
|
||||
*
|
||||
* Searches through the whole passed metadata for the first WordListMetadata associated
|
||||
* with the passed ID. If several metadata chunks with the same id are found, it will
|
||||
* always return the one with the bigger FormatVersion that is less or equal than the
|
||||
* maximum supported format version (as listed in UpdateHandler).
|
||||
* This will NEVER return the metadata with a FormatVersion bigger than what is supported,
|
||||
* even if it is the only word list with this ID.
|
||||
*
|
||||
* @param metadata the metadata to search into.
|
||||
* @param id the word list ID of the metadata to find.
|
||||
* @return the associated metadata, or null if not found.
|
||||
*/
|
||||
fun findWordListById(metadata: List<WordListMetadata>,
|
||||
id: String): WordListMetadata? {
|
||||
var bestWordList: WordListMetadata? = null
|
||||
var bestFormatVersion = Int.MIN_VALUE // To be sure we can't be inadvertently smaller
|
||||
for (wordList in metadata) {
|
||||
if (id == wordList.mId && wordList.mFormatVersion > bestFormatVersion) {
|
||||
bestWordList = wordList
|
||||
bestFormatVersion = wordList.mFormatVersion
|
||||
}
|
||||
}
|
||||
// If we didn't find any match we'll return null.
|
||||
return bestWordList
|
||||
}
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
|
||||
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.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Helper class containing functions to parse the dictionary metadata.
|
||||
*/
|
||||
public class MetadataParser {
|
||||
|
||||
// Name of the fields in the JSON-formatted file.
|
||||
private static final String ID_FIELD_NAME = MetadataDbHelper.WORDLISTID_COLUMN;
|
||||
private static final String LOCALE_FIELD_NAME = "locale";
|
||||
private static final String DESCRIPTION_FIELD_NAME = MetadataDbHelper.DESCRIPTION_COLUMN;
|
||||
private static final String UPDATE_FIELD_NAME = "update";
|
||||
private static final String FILESIZE_FIELD_NAME = MetadataDbHelper.FILESIZE_COLUMN;
|
||||
private static final String RAW_CHECKSUM_FIELD_NAME = MetadataDbHelper.RAW_CHECKSUM_COLUMN;
|
||||
private static final String CHECKSUM_FIELD_NAME = MetadataDbHelper.CHECKSUM_COLUMN;
|
||||
private static final String REMOTE_FILENAME_FIELD_NAME =
|
||||
MetadataDbHelper.REMOTE_FILENAME_COLUMN;
|
||||
private static final String VERSION_FIELD_NAME = MetadataDbHelper.VERSION_COLUMN;
|
||||
private static final String FORMATVERSION_FIELD_NAME = MetadataDbHelper.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.
|
||||
*/
|
||||
private static WordListMetadata parseOneWordList(final JsonReader reader)
|
||||
throws IOException, BadFormatException {
|
||||
final TreeMap<String, String> arguments = new TreeMap<>();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
final String name = reader.nextName();
|
||||
if (!TextUtils.isEmpty(name)) {
|
||||
arguments.put(name, reader.nextString());
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
if (TextUtils.isEmpty(arguments.get(ID_FIELD_NAME))
|
||||
|| TextUtils.isEmpty(arguments.get(LOCALE_FIELD_NAME))
|
||||
|| TextUtils.isEmpty(arguments.get(DESCRIPTION_FIELD_NAME))
|
||||
|| TextUtils.isEmpty(arguments.get(UPDATE_FIELD_NAME))
|
||||
|| TextUtils.isEmpty(arguments.get(FILESIZE_FIELD_NAME))
|
||||
|| TextUtils.isEmpty(arguments.get(CHECKSUM_FIELD_NAME))
|
||||
|| TextUtils.isEmpty(arguments.get(REMOTE_FILENAME_FIELD_NAME))
|
||||
|| TextUtils.isEmpty(arguments.get(VERSION_FIELD_NAME))
|
||||
|| TextUtils.isEmpty(arguments.get(FORMATVERSION_FIELD_NAME))) {
|
||||
throw new 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 new WordListMetadata(
|
||||
arguments.get(ID_FIELD_NAME),
|
||||
MetadataDbHelper.TYPE_BULK,
|
||||
arguments.get(DESCRIPTION_FIELD_NAME),
|
||||
Long.parseLong(arguments.get(UPDATE_FIELD_NAME)),
|
||||
Long.parseLong(arguments.get(FILESIZE_FIELD_NAME)),
|
||||
arguments.get(RAW_CHECKSUM_FIELD_NAME),
|
||||
arguments.get(CHECKSUM_FIELD_NAME),
|
||||
MetadataDbHelper.DICTIONARY_RETRY_THRESHOLD /* retryCount */,
|
||||
null,
|
||||
arguments.get(REMOTE_FILENAME_FIELD_NAME),
|
||||
Integer.parseInt(arguments.get(VERSION_FIELD_NAME)),
|
||||
Integer.parseInt(arguments.get(FORMATVERSION_FIELD_NAME)),
|
||||
0, arguments.get(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.
|
||||
*/
|
||||
public static List<WordListMetadata> parseMetadata(final InputStreamReader input)
|
||||
throws IOException, BadFormatException {
|
||||
JsonReader reader = new JsonReader(input);
|
||||
final ArrayList<WordListMetadata> readInfo = new ArrayList<>();
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
final WordListMetadata thisMetadata = parseOneWordList(reader);
|
||||
if (!TextUtils.isEmpty(thisMetadata.mLocale))
|
||||
readInfo.add(thisMetadata);
|
||||
}
|
||||
return Collections.unmodifiableList(readInfo);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -13,17 +13,15 @@
|
|||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package org.dslul.openboard.inputmethod.dictionarypack
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* Helper to get the metadata URI from its base URI.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class MetadataUriGetter {
|
||||
public static String getUri(final Context context, final String baseUri) {
|
||||
return baseUri;
|
||||
object MetadataUriGetter {
|
||||
fun getUri(context: Context?, baseUri: String): String {
|
||||
return baseUri
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
|
||||
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.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Class to keep long-term log. This is inactive in production, and is only for debug purposes.
|
||||
*/
|
||||
public class PrivateLog {
|
||||
|
||||
public static final boolean DEBUG = false;
|
||||
|
||||
private static final String LOG_DATABASE_NAME = "log";
|
||||
private static final String LOG_TABLE_NAME = "log";
|
||||
private static final int LOG_DATABASE_VERSION = 1;
|
||||
|
||||
private static final String COLUMN_DATE = "date";
|
||||
private static final String COLUMN_EVENT = "event";
|
||||
|
||||
private static final String LOG_TABLE_CREATE = "CREATE TABLE " + LOG_TABLE_NAME + " ("
|
||||
+ COLUMN_DATE + " TEXT,"
|
||||
+ COLUMN_EVENT + " TEXT);";
|
||||
|
||||
static final SimpleDateFormat sDateFormat = new SimpleDateFormat(
|
||||
"yyyy/MM/dd HH:mm:ss", Locale.ROOT);
|
||||
|
||||
private static PrivateLog sInstance = new PrivateLog();
|
||||
private static DebugHelper sDebugHelper = null;
|
||||
|
||||
private PrivateLog() {
|
||||
}
|
||||
|
||||
public static synchronized PrivateLog getInstance(final Context context) {
|
||||
if (!DEBUG) return sInstance;
|
||||
synchronized(PrivateLog.class) {
|
||||
if (sDebugHelper == null) {
|
||||
sDebugHelper = new DebugHelper(context);
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
}
|
||||
|
||||
static class DebugHelper extends SQLiteOpenHelper {
|
||||
|
||||
DebugHelper(final Context context) {
|
||||
super(context, LOG_DATABASE_NAME, null, LOG_DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
if (!DEBUG) return;
|
||||
db.execSQL(LOG_TABLE_CREATE);
|
||||
insert(db, "Created table");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (!DEBUG) return;
|
||||
// Remove all data.
|
||||
db.execSQL("DROP TABLE IF EXISTS " + LOG_TABLE_NAME);
|
||||
onCreate(db);
|
||||
insert(db, "Upgrade finished");
|
||||
}
|
||||
|
||||
static void insert(SQLiteDatabase db, String event) {
|
||||
if (!DEBUG) return;
|
||||
final ContentValues c = new ContentValues(2);
|
||||
c.put(COLUMN_DATE, sDateFormat.format(new Date(System.currentTimeMillis())));
|
||||
c.put(COLUMN_EVENT, event);
|
||||
db.insert(LOG_TABLE_NAME, null, c);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void log(String event) {
|
||||
if (!DEBUG) return;
|
||||
final SQLiteDatabase l = sDebugHelper.getWritableDatabase();
|
||||
DebugHelper.insert(l, event);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,12 +13,11 @@
|
|||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
package org.dslul.openboard.inputmethod.dictionarypack
|
||||
|
||||
/**
|
||||
* A simple interface to report problems.
|
||||
*/
|
||||
public interface ProblemReporter {
|
||||
public void report(Exception e);
|
||||
}
|
||||
interface ProblemReporter {
|
||||
fun report(e: Exception?)
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
|
||||
import android.content.ContentValues;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* The metadata for a single word list.
|
||||
*
|
||||
* Instances of this class are always immutable.
|
||||
*/
|
||||
public class WordListMetadata {
|
||||
|
||||
public final String mId;
|
||||
public final int mType; // Type, as of MetadataDbHelper#TYPE_*
|
||||
public final String mDescription;
|
||||
public final long mLastUpdate;
|
||||
public final long mFileSize;
|
||||
public final String mRawChecksum;
|
||||
public final String mChecksum;
|
||||
public final String mLocalFilename;
|
||||
public final String mRemoteFilename;
|
||||
public final int mVersion; // version of this word list
|
||||
public final int mFlags; // Always 0 in this version, reserved for future use
|
||||
public int mRetryCount;
|
||||
|
||||
// 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.
|
||||
public final String mLocale;
|
||||
|
||||
|
||||
// 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.
|
||||
public final int mFormatVersion;
|
||||
|
||||
public WordListMetadata(final String id, final int type,
|
||||
final String description, final long lastUpdate, final long fileSize,
|
||||
final String rawChecksum, final String checksum, final int retryCount,
|
||||
final String localFilename, final String remoteFilename,
|
||||
final int version, final int formatVersion,
|
||||
final int flags, final String locale) {
|
||||
mId = id;
|
||||
mType = type;
|
||||
mDescription = description;
|
||||
mLastUpdate = lastUpdate; // In milliseconds
|
||||
mFileSize = fileSize;
|
||||
mRawChecksum = rawChecksum;
|
||||
mChecksum = checksum;
|
||||
mRetryCount = retryCount;
|
||||
mLocalFilename = localFilename;
|
||||
mRemoteFilename = remoteFilename;
|
||||
mVersion = version;
|
||||
mFormatVersion = formatVersion;
|
||||
mFlags = flags;
|
||||
mLocale = locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a WordListMetadata from the contents of a ContentValues.
|
||||
*
|
||||
* If this lacks any required field, IllegalArgumentException is thrown.
|
||||
*/
|
||||
public static WordListMetadata createFromContentValues(@Nonnull final ContentValues values) {
|
||||
final String id = values.getAsString(MetadataDbHelper.WORDLISTID_COLUMN);
|
||||
final Integer type = values.getAsInteger(MetadataDbHelper.TYPE_COLUMN);
|
||||
final String description = values.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN);
|
||||
final Long lastUpdate = values.getAsLong(MetadataDbHelper.DATE_COLUMN);
|
||||
final Long fileSize = values.getAsLong(MetadataDbHelper.FILESIZE_COLUMN);
|
||||
final String rawChecksum = values.getAsString(MetadataDbHelper.RAW_CHECKSUM_COLUMN);
|
||||
final String checksum = values.getAsString(MetadataDbHelper.CHECKSUM_COLUMN);
|
||||
final int retryCount = values.getAsInteger(MetadataDbHelper.RETRY_COUNT_COLUMN);
|
||||
final String localFilename = values.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
|
||||
final String remoteFilename = values.getAsString(MetadataDbHelper.REMOTE_FILENAME_COLUMN);
|
||||
final Integer version = values.getAsInteger(MetadataDbHelper.VERSION_COLUMN);
|
||||
final Integer formatVersion = values.getAsInteger(MetadataDbHelper.FORMATVERSION_COLUMN);
|
||||
final Integer flags = values.getAsInteger(MetadataDbHelper.FLAGS_COLUMN);
|
||||
final String locale = values.getAsString(MetadataDbHelper.LOCALE_COLUMN);
|
||||
if (null == id
|
||||
|| null == type
|
||||
|| null == description
|
||||
|| null == lastUpdate
|
||||
|| null == fileSize
|
||||
|| null == checksum
|
||||
|| null == localFilename
|
||||
|| null == remoteFilename
|
||||
|| null == version
|
||||
|| null == formatVersion
|
||||
|| null == flags
|
||||
|| null == locale) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return new WordListMetadata(id, type, description, lastUpdate, fileSize, rawChecksum,
|
||||
checksum, retryCount, localFilename, remoteFilename, version, formatVersion,
|
||||
flags, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder(WordListMetadata.class.getSimpleName());
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
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.Companion.WORDLISTID_COLUMN)
|
||||
val type = values.getAsInteger(MetadataDbHelper.Companion.TYPE_COLUMN)
|
||||
val description = values.getAsString(MetadataDbHelper.Companion.DESCRIPTION_COLUMN)
|
||||
val lastUpdate = values.getAsLong(MetadataDbHelper.Companion.DATE_COLUMN)
|
||||
val fileSize = values.getAsLong(MetadataDbHelper.Companion.FILESIZE_COLUMN)
|
||||
val rawChecksum = values.getAsString(MetadataDbHelper.Companion.RAW_CHECKSUM_COLUMN)
|
||||
val checksum = values.getAsString(MetadataDbHelper.Companion.CHECKSUM_COLUMN)
|
||||
val retryCount = values.getAsInteger(MetadataDbHelper.Companion.RETRY_COUNT_COLUMN)
|
||||
val localFilename = values.getAsString(MetadataDbHelper.Companion.LOCAL_FILENAME_COLUMN)
|
||||
val remoteFilename = values.getAsString(MetadataDbHelper.Companion.REMOTE_FILENAME_COLUMN)
|
||||
val version = values.getAsInteger(MetadataDbHelper.Companion.VERSION_COLUMN)
|
||||
val formatVersion = values.getAsInteger(MetadataDbHelper.Companion.FORMATVERSION_COLUMN)
|
||||
val flags = values.getAsInteger(MetadataDbHelper.Companion.FLAGS_COLUMN)
|
||||
val locale = values.getAsString(MetadataDbHelper.Companion.LOCALE_COLUMN)
|
||||
require(!(null == id || null == type || null == description || null == lastUpdate || null == fileSize || null == checksum || null == localFilename || null == remoteFilename || null == version || null == formatVersion || null == flags || null == locale))
|
||||
return WordListMetadata(id, type, description, lastUpdate, fileSize, rawChecksum,
|
||||
checksum, retryCount, localFilename, remoteFilename, version, formatVersion,
|
||||
flags, locale)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,296 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.Preference;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.dslul.openboard.inputmethod.latin.R;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public final class WordListPreference extends Preference {
|
||||
private static final String TAG = WordListPreference.class.getSimpleName();
|
||||
|
||||
// What to display in the "status" field when we receive unknown data as a status from
|
||||
// the content provider. Empty string sounds sensible.
|
||||
private static final String NO_STATUS_MESSAGE = "";
|
||||
|
||||
/// Actions
|
||||
private static final int ACTION_UNKNOWN = 0;
|
||||
private static final int ACTION_ENABLE_DICT = 1;
|
||||
private static final int ACTION_DISABLE_DICT = 2;
|
||||
private static final int ACTION_DELETE_DICT = 3;
|
||||
|
||||
// Members
|
||||
// The metadata word list id and version of this word list.
|
||||
public final String mWordlistId;
|
||||
public final int mVersion;
|
||||
public final Locale mLocale;
|
||||
public final String mDescription;
|
||||
|
||||
// The id of the client for which this preference is.
|
||||
private final String mClientId;
|
||||
// The status
|
||||
private int mStatus;
|
||||
// The size of the dictionary file
|
||||
private final int mFilesize;
|
||||
|
||||
private final DictionaryListInterfaceState mInterfaceState;
|
||||
|
||||
public WordListPreference(final Context context,
|
||||
final DictionaryListInterfaceState dictionaryListInterfaceState, final String clientId,
|
||||
final String wordlistId, final int version, final Locale locale,
|
||||
final String description, final int status, final int filesize) {
|
||||
super(context, null);
|
||||
mInterfaceState = dictionaryListInterfaceState;
|
||||
mClientId = clientId;
|
||||
mVersion = version;
|
||||
mWordlistId = wordlistId;
|
||||
mFilesize = filesize;
|
||||
mLocale = locale;
|
||||
mDescription = description;
|
||||
|
||||
setLayoutResource(R.layout.dictionary_line);
|
||||
|
||||
setTitle(description);
|
||||
setStatus(status);
|
||||
setKey(wordlistId);
|
||||
}
|
||||
|
||||
public void setStatus(final int status) {
|
||||
if (status == mStatus) return;
|
||||
mStatus = status;
|
||||
setSummary(getSummary(status));
|
||||
}
|
||||
|
||||
public boolean hasStatus(final int status) {
|
||||
return status == mStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final ViewGroup parent) {
|
||||
final View orphanedView = mInterfaceState.findFirstOrphanedView();
|
||||
if (null != orphanedView) return orphanedView; // Will be sent to onBindView
|
||||
final View newView = super.onCreateView(parent);
|
||||
return mInterfaceState.addToCacheAndReturnView(newView);
|
||||
}
|
||||
|
||||
public boolean hasPriorityOver(final int otherPrefStatus) {
|
||||
// Both of these should be one of MetadataDbHelper.STATUS_*
|
||||
return mStatus > otherPrefStatus;
|
||||
}
|
||||
|
||||
private String getSummary(final int status) {
|
||||
final Context context = getContext();
|
||||
switch (status) {
|
||||
// If we are deleting the word list, for the user it's like it's already deleted.
|
||||
// It should be reinstallable. Exposing to the user the whole complexity of
|
||||
// the delayed deletion process between the dictionary pack and Android Keyboard
|
||||
// would only be confusing.
|
||||
case MetadataDbHelper.STATUS_DELETING:
|
||||
case MetadataDbHelper.STATUS_AVAILABLE:
|
||||
return context.getString(R.string.dictionary_available);
|
||||
case MetadataDbHelper.STATUS_DOWNLOADING:
|
||||
return context.getString(R.string.dictionary_downloading);
|
||||
case MetadataDbHelper.STATUS_INSTALLED:
|
||||
return context.getString(R.string.dictionary_installed);
|
||||
case MetadataDbHelper.STATUS_DISABLED:
|
||||
return context.getString(R.string.dictionary_disabled);
|
||||
default:
|
||||
return NO_STATUS_MESSAGE;
|
||||
}
|
||||
}
|
||||
|
||||
// The table below needs to be kept in sync with MetadataDbHelper.STATUS_* since it uses
|
||||
// the values as indices.
|
||||
private static final int sStatusActionList[][] = {
|
||||
// MetadataDbHelper.STATUS_UNKNOWN
|
||||
{},
|
||||
// MetadataDbHelper.STATUS_AVAILABLE
|
||||
{ ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT },
|
||||
// MetadataDbHelper.STATUS_DOWNLOADING
|
||||
{ ButtonSwitcher.STATUS_CANCEL, ACTION_DISABLE_DICT },
|
||||
// MetadataDbHelper.STATUS_INSTALLED
|
||||
{ ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT },
|
||||
// MetadataDbHelper.STATUS_DISABLED
|
||||
{ ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT },
|
||||
// MetadataDbHelper.STATUS_DELETING
|
||||
// We show 'install' because the file is supposed to be deleted.
|
||||
// The user may reinstall it.
|
||||
{ ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT }
|
||||
};
|
||||
|
||||
static int getButtonSwitcherStatus(final int status) {
|
||||
if (status >= sStatusActionList.length) {
|
||||
Log.e(TAG, "Unknown status " + status);
|
||||
return ButtonSwitcher.STATUS_NO_BUTTON;
|
||||
}
|
||||
return sStatusActionList[status][0];
|
||||
}
|
||||
|
||||
static int getActionIdFromStatusAndMenuEntry(final int status) {
|
||||
if (status >= sStatusActionList.length) {
|
||||
Log.e(TAG, "Unknown status " + status);
|
||||
return ACTION_UNKNOWN;
|
||||
}
|
||||
return sStatusActionList[status][1];
|
||||
}
|
||||
|
||||
private void disableDict() {
|
||||
final Context context = getContext();
|
||||
final SharedPreferences 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 void enableDict() {
|
||||
final Context context = getContext();
|
||||
final SharedPreferences 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 void deleteDict() {
|
||||
final Context context = getContext();
|
||||
final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
|
||||
CommonPreferences.disable(prefs, mWordlistId);
|
||||
setStatus(MetadataDbHelper.STATUS_DELETING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindView(final View view) {
|
||||
super.onBindView(view);
|
||||
((ViewGroup)view).setLayoutTransition(null);
|
||||
|
||||
final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)view.findViewById(
|
||||
R.id.wordlist_button_switcher);
|
||||
// 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.
|
||||
final int 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(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
onActionButtonClicked();
|
||||
}
|
||||
});
|
||||
view.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
onWordListClicked(v);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onWordListClicked(final View v) {
|
||||
// Note : v is the preference view
|
||||
final ViewParent parent = v.getParent();
|
||||
// Just in case something changed in the framework, test for the concrete class
|
||||
if (!(parent instanceof ListView)) return;
|
||||
final ListView listView = (ListView)parent;
|
||||
final int indexToOpen;
|
||||
// Close all first, we'll open back any item that needs to be open.
|
||||
final boolean wasOpen = mInterfaceState.isOpen(mWordlistId);
|
||||
mInterfaceState.closeAll();
|
||||
if (wasOpen) {
|
||||
// This button being shown. Take note that we don't want to open any button in the
|
||||
// loop below.
|
||||
indexToOpen = -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);
|
||||
indexToOpen = listView.indexOfChild(v);
|
||||
}
|
||||
final int lastDisplayedIndex =
|
||||
listView.getLastVisiblePosition() - listView.getFirstVisiblePosition();
|
||||
// The "lastDisplayedIndex" is actually displayed, hence the <=
|
||||
for (int i = 0; i <= lastDisplayedIndex; ++i) {
|
||||
final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)listView.getChildAt(i)
|
||||
.findViewById(R.id.wordlist_button_switcher);
|
||||
if (i == indexToOpen) {
|
||||
buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus));
|
||||
} else {
|
||||
buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onActionButtonClicked() {
|
||||
switch (getActionIdFromStatusAndMenuEntry(mStatus)) {
|
||||
case ACTION_ENABLE_DICT:
|
||||
enableDict();
|
||||
break;
|
||||
case ACTION_DISABLE_DICT:
|
||||
disableDict();
|
||||
break;
|
||||
case ACTION_DELETE_DICT:
|
||||
deleteDict();
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Unknown menu item pressed");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
/**
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.Companion.STATUS_DELETING, MetadataDbHelper.Companion.STATUS_AVAILABLE -> context.getString(R.string.dictionary_available)
|
||||
MetadataDbHelper.Companion.STATUS_DOWNLOADING -> context.getString(R.string.dictionary_downloading)
|
||||
MetadataDbHelper.Companion.STATUS_INSTALLED -> context.getString(R.string.dictionary_installed)
|
||||
MetadataDbHelper.Companion.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.Companion.STATUS_DOWNLOADING == mStatus) {
|
||||
setStatus(MetadataDbHelper.Companion.STATUS_AVAILABLE)
|
||||
} else if (MetadataDbHelper.Companion.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.Companion.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.Companion.STATUS_AVAILABLE == mStatus) {
|
||||
setStatus(MetadataDbHelper.Companion.STATUS_DOWNLOADING)
|
||||
} else if (MetadataDbHelper.Companion.STATUS_DISABLED == mStatus
|
||||
|| MetadataDbHelper.Companion.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.Companion.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.Companion.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.Companion.STATUS_NO_BUTTON)
|
||||
}
|
||||
buttonSwitcher.setInternalOnClickListener(View.OnClickListener { onActionButtonClicked() })
|
||||
view.setOnClickListener { v -> onWordListClicked(v) }
|
||||
}
|
||||
|
||||
fun onWordListClicked(v: View) { // Note : v is the preference view
|
||||
val parent = v.parent as? ListView ?: return
|
||||
// Just in case something changed in the framework, test for the concrete class
|
||||
val listView = parent
|
||||
val indexToOpen: Int
|
||||
// Close all first, we'll open back any item that needs to be open.
|
||||
val wasOpen = mInterfaceState.isOpen(mWordlistId)
|
||||
mInterfaceState.closeAll()
|
||||
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.Companion.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.Companion.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
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue