aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/dictionarypack
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/com/android/inputmethod/dictionarypack')
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/ActionBatch.java26
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java155
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java178
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java67
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java16
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryService.java5
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java9
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/LogProblemReporter.java3
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java92
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/MetadataUriGetter.java33
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/PrivateLog.java23
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java29
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/WordListPreference.java129
13 files changed, 642 insertions, 123 deletions
diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
index df4a52f4e..bf2230553 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
@@ -138,7 +138,12 @@ public final class ActionBatch {
if (null == manager) return;
// This is an upgraded word list: we should download it.
- final Uri uri = Uri.parse(mWordList.mRemoteFilename);
+ // Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
+ // DownloadManager also stupidly cuts the extension to replace with its own that it
+ // gets from the content-type. We need to circumvent this.
+ final String disambiguator = "#" + System.currentTimeMillis()
+ + com.android.inputmethod.latin.Utils.getVersionName(context) + ".dict";
+ final Uri uri = Uri.parse(mWordList.mRemoteFilename + disambiguator);
final Request request = new Request(uri);
final Resources res = context.getResources();
@@ -174,7 +179,7 @@ public final class ActionBatch {
final long downloadId = UpdateHandler.registerDownloadRequest(manager, request, db,
mWordList.mId, mWordList.mVersion);
Utils.l("Starting download of", uri, "with id", downloadId);
- PrivateLog.log("Starting download of " + uri + ", id : " + downloadId, context);
+ PrivateLog.log("Starting download of " + uri + ", id : " + downloadId);
}
}
@@ -333,7 +338,7 @@ public final class ActionBatch {
mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mChecksum,
mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion);
PrivateLog.log("Insert 'available' record for " + mWordList.mDescription
- + " and locale " + mWordList.mLocale, context);
+ + " and locale " + mWordList.mLocale);
db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values);
}
}
@@ -383,7 +388,7 @@ public final class ActionBatch {
mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion,
mWordList.mFormatVersion);
PrivateLog.log("Insert 'preinstalled' record for " + mWordList.mDescription
- + " and locale " + mWordList.mLocale, context);
+ + " and locale " + mWordList.mLocale);
db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values);
}
}
@@ -424,7 +429,7 @@ public final class ActionBatch {
mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mChecksum,
mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion);
PrivateLog.log("Updating record for " + mWordList.mDescription
- + " and locale " + mWordList.mLocale, context);
+ + " and locale " + mWordList.mLocale);
db.update(MetadataDbHelper.METADATA_TABLE_NAME, values,
MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND "
+ MetadataDbHelper.VERSION_COLUMN + " = ?",
@@ -478,13 +483,14 @@ public final class ActionBatch {
if (MetadataDbHelper.STATUS_INSTALLED == status
|| MetadataDbHelper.STATUS_DISABLED == status
|| MetadataDbHelper.STATUS_DELETING == status) {
- // If it is installed or disabled, then we cannot remove the entry lest the user
- // lose the ability to delete the file or otherwise administrate it. We will thus
- // leave it as is, but remove the URI from the database since it is not supposed to
- // be accessible any more.
+ // 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
- // Android Keyboard actually has deleted it before we can remove its metadata.
+ // 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 + " = ?",
diff --git a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
new file mode 100644
index 000000000..5ab94a429
--- /dev/null
+++ b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
@@ -0,0 +1,155 @@
+/**
+ * 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 com.android.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 com.android.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 OnClickListener mOnClickListener;
+
+ public ButtonSwitcher(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ButtonSwitcher(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @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);
+ mInstallButton.setOnClickListener(mOnClickListener);
+ mCancelButton.setOnClickListener(mOnClickListener);
+ mDeleteButton.setOnClickListener(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);
+ }
+
+ 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) {
+ if (newStatus != mStatus) return;
+ animateButton(newButton, ANIMATION_IN);
+ }
+ });
+ } else if (null != oldButton) {
+ animateButton(oldButton, ANIMATION_OUT);
+ } else if (null != newButton) {
+ animateButton(newButton, ANIMATION_IN);
+ }
+ }
+
+ public void setInternalOnClickListener(final OnClickListener listener) {
+ mOnClickListener = listener;
+ }
+
+ private ViewPropertyAnimator animateButton(final View button, final int direction) {
+ final float outerX = getWidth();
+ final float innerX = button.getX() - button.getTranslationX();
+ if (ANIMATION_IN == direction) {
+ button.setClickable(true);
+ return button.animate().translationX(0);
+ } else {
+ button.setClickable(false);
+ return button.animate().translationX(outerX - innerX);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java
new file mode 100644
index 000000000..88b5032e3
--- /dev/null
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java
@@ -0,0 +1,178 @@
+/**
+ * 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 com.android.inputmethod.dictionarypack;
+
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.ProgressBar;
+
+public class DictionaryDownloadProgressBar extends ProgressBar {
+ @SuppressWarnings("unused")
+ private static final String TAG = DictionaryDownloadProgressBar.class.getSimpleName();
+ private static final int NOT_A_DOWNLOADMANAGER_PENDING_ID = 0;
+
+ private String mClientId;
+ private String mWordlistId;
+ private boolean mIsCurrentlyAttachedToWindow = false;
+ private Thread mReporterThread = null;
+
+ public DictionaryDownloadProgressBar(final Context context) {
+ super(context);
+ }
+
+ public DictionaryDownloadProgressBar(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setIds(final String clientId, final String wordlistId) {
+ mClientId = clientId;
+ mWordlistId = wordlistId;
+ }
+
+ static private int getDownloadManagerPendingIdFromWordlistId(final Context context,
+ final String clientId, final String wordlistId) {
+ final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId);
+ final ContentValues wordlistValues =
+ MetadataDbHelper.getContentValuesOfLatestAvailableWordlistById(db, wordlistId);
+ if (null == wordlistValues) {
+ // We don't know anything about a word list with this id. Bug? This should never
+ // happen, but still return to prevent a crash.
+ Log.e(TAG, "Unexpected word list ID: " + wordlistId);
+ return NOT_A_DOWNLOADMANAGER_PENDING_ID;
+ }
+ return wordlistValues.getAsInteger(MetadataDbHelper.PENDINGID_COLUMN);
+ }
+
+ /*
+ * This method will stop any running updater thread for this progress bar and create and run
+ * a new one only if the progress bar is visible.
+ * Hence, as a result of calling this method, the progress bar will have an updater thread
+ * running if and only if the progress bar is visible.
+ */
+ private void updateReporterThreadRunningStatusAccordingToVisibility() {
+ if (null != mReporterThread) mReporterThread.interrupt();
+ if (mIsCurrentlyAttachedToWindow && View.VISIBLE == getVisibility()) {
+ final int downloadManagerPendingId =
+ getDownloadManagerPendingIdFromWordlistId(getContext(), mClientId, mWordlistId);
+ if (NOT_A_DOWNLOADMANAGER_PENDING_ID == downloadManagerPendingId) {
+ // Can't get the ID. This is never supposed to happen, but still clear the updater
+ // thread and return to avoid a crash.
+ mReporterThread = null;
+ return;
+ }
+ final UpdaterThread updaterThread =
+ new UpdaterThread(getContext(), downloadManagerPendingId);
+ updaterThread.start();
+ mReporterThread = updaterThread;
+ } else {
+ // We're not going to restart the thread anyway, so we may as well garbage collect it.
+ mReporterThread = null;
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ mIsCurrentlyAttachedToWindow = true;
+ updateReporterThreadRunningStatusAccordingToVisibility();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ mIsCurrentlyAttachedToWindow = false;
+ updateReporterThreadRunningStatusAccordingToVisibility();
+ }
+
+ private class UpdaterThread extends Thread {
+ private final static int REPORT_PERIOD = 150; // how often to report progress, in ms
+ final DownloadManager mDownloadManager;
+ final int mId;
+ public UpdaterThread(final Context context, final int id) {
+ super();
+ mDownloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+ mId = id;
+ }
+ @Override
+ public void run() {
+ try {
+ // It's almost impossible that mDownloadManager is null (it would mean it has been
+ // disabled between pressing the 'install' button and displaying the progress
+ // bar), but just in case.
+ if (null == mDownloadManager) return;
+ final UpdateHelper updateHelper = new UpdateHelper();
+ final Query query = new Query().setFilterById(mId);
+ int lastProgress = 0;
+ setIndeterminate(true);
+ while (!isInterrupted()) {
+ final Cursor cursor = mDownloadManager.query(query);
+ if (null == cursor) {
+ // Can't contact DownloadManager: this should never happen.
+ return;
+ }
+ try {
+ if (cursor.moveToNext()) {
+ final int columnBytesDownloadedSoFar = cursor.getColumnIndex(
+ DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
+ final int bytesDownloadedSoFar =
+ cursor.getInt(columnBytesDownloadedSoFar);
+ updateHelper.setProgressFromAnotherThread(bytesDownloadedSoFar);
+ } else {
+ // Download has finished and DownloadManager has already been asked to
+ // clean up the db entry.
+ updateHelper.setProgressFromAnotherThread(getMax());
+ return;
+ }
+ } finally {
+ cursor.close();
+ }
+ Thread.sleep(REPORT_PERIOD);
+ }
+ } catch (InterruptedException e) {
+ // Do nothing and terminate normally.
+ }
+ }
+
+ private class UpdateHelper implements Runnable {
+ private int mProgress;
+ @Override
+ public void run() {
+ setIndeterminate(false);
+ setProgress(mProgress);
+ }
+ public void setProgressFromAnotherThread(final int progress) {
+ if (mProgress != progress) {
+ mProgress = progress;
+ // For some unknown reason, setProgress just does not work from a separate
+ // thread, although the code in ProgressBar looks like it should. Thus, we
+ // resort to a runnable posted to the handler of the view.
+ final Handler handler = getHandler();
+ // It's possible to come here before this view has been laid out. If so,
+ // just ignore the call - it will be updated again later.
+ if (null == handler) return;
+ handler.post(this);
+ }
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
new file mode 100644
index 000000000..de3711c27
--- /dev/null
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
@@ -0,0 +1,67 @@
+/**
+ * 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 com.android.inputmethod.dictionarypack;
+
+import com.android.inputmethod.latin.CollectionUtils;
+
+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 {
+ private static class State {
+ public boolean mOpen = false;
+ public int mStatus = MetadataDbHelper.STATUS_UNKNOWN;
+ }
+
+ private HashMap<String, State> mWordlistToState = CollectionUtils.newHashMap();
+
+ 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;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
index f8d1c4fc9..4fbe16233 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
@@ -189,7 +189,7 @@ public final class DictionaryProvider extends ContentProvider {
*/
@Override
public String getType(final Uri uri) {
- PrivateLog.log("Asked for type of : " + uri, this);
+ PrivateLog.log("Asked for type of : " + uri);
final int match = matchUri(uri);
switch (match) {
case NO_MATCH: return null;
@@ -220,7 +220,7 @@ public final class DictionaryProvider extends ContentProvider {
public Cursor query(final Uri uri, final String[] projection, final String selection,
final String[] selectionArgs, final String sortOrder) {
Utils.l("Uri =", uri);
- PrivateLog.log("Query : " + uri, this);
+ PrivateLog.log("Query : " + uri);
final String clientId = getClientId(uri);
final int match = matchUri(uri);
switch (match) {
@@ -228,7 +228,7 @@ public final class DictionaryProvider extends ContentProvider {
case DICTIONARY_V2_WHOLE_LIST:
final Cursor c = MetadataDbHelper.queryDictionaries(getContext(), clientId);
Utils.l("List of dictionaries with count", c.getCount());
- PrivateLog.log("Returned a list of " + c.getCount() + " items", this);
+ 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
@@ -248,10 +248,10 @@ public final class DictionaryProvider extends ContentProvider {
// TODO: pass clientId to the following function
DictionaryService.updateNowIfNotUpdatedInAVeryLongTime(getContext());
if (null != dictFiles && dictFiles.size() > 0) {
- PrivateLog.log("Returned " + dictFiles.size() + " files", this);
+ PrivateLog.log("Returned " + dictFiles.size() + " files");
return new ResourcePathCursor(dictFiles);
} else {
- PrivateLog.log("No dictionary files for this URL", this);
+ PrivateLog.log("No dictionary files for this URL");
return new ResourcePathCursor(Collections.<WordListInfo>emptyList());
}
// V2_METADATA and V2_DATAFILE are not supported for query()
@@ -488,7 +488,7 @@ public final class DictionaryProvider extends ContentProvider {
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(), this);
+ PrivateLog.log("Insert, uri = " + uri.toString());
final String clientId = getClientId(uri);
switch (matchUri(uri)) {
case DICTIONARY_V2_METADATA:
@@ -517,7 +517,7 @@ public final class DictionaryProvider extends ContentProvider {
break;
case DICTIONARY_V1_WHOLE_LIST:
case DICTIONARY_V1_DICT_INFO:
- PrivateLog.log("Attempt to insert : " + uri, this);
+ PrivateLog.log("Attempt to insert : " + uri);
throw new UnsupportedOperationException(
"Insertion in the dictionary is not supported in this version");
}
@@ -532,7 +532,7 @@ public final class DictionaryProvider extends ContentProvider {
@Override
public int update(final Uri uri, final ContentValues values, final String selection,
final String[] selectionArgs) throws UnsupportedOperationException {
- PrivateLog.log("Attempt to update : " + uri, this);
+ PrivateLog.log("Attempt to update : " + uri);
throw new UnsupportedOperationException("Updating dictionary words is not supported");
}
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
index 5817eb498..46bb5543a 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
@@ -21,7 +21,6 @@ import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.os.IBinder;
import android.text.format.DateUtils;
import android.util.Log;
@@ -190,7 +189,7 @@ public final class DictionaryService extends Service {
// is still more recent than UPDATE_FREQUENCY, do nothing.
if (!isLastUpdateAtLeastThisOld(context, UPDATE_FREQUENCY)) return;
- PrivateLog.log("Date changed - registering alarm", context);
+ PrivateLog.log("Date changed - registering alarm");
AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
// Best effort to wake between midnight and MAX_ALARM_DELAY in the morning.
@@ -215,7 +214,7 @@ public final class DictionaryService extends Service {
private static boolean isLastUpdateAtLeastThisOld(final Context context, final long time) {
final long now = System.currentTimeMillis();
final long lastUpdate = MetadataDbHelper.getOldestUpdateTime(context);
- PrivateLog.log("Last update was " + lastUpdate, context);
+ PrivateLog.log("Last update was " + lastUpdate);
return lastUpdate + time < now;
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
index 9e27c1f3f..fb75d6dc0 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
@@ -64,6 +64,8 @@ public final class DictionarySettingsFragment extends PreferenceFragment
private ConnectivityManager mConnectivityManager;
private MenuItem mUpdateNowMenu;
private boolean mChangedSettings;
+ private DictionaryListInterfaceState mDictionaryListInterfaceState =
+ new DictionaryListInterfaceState();
private final BroadcastReceiver mConnectivityChangedReceiver = new BroadcastReceiver() {
@Override
@@ -283,6 +285,7 @@ public final class DictionarySettingsFragment extends PreferenceFragment
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);
@@ -292,13 +295,15 @@ public final class DictionarySettingsFragment extends PreferenceFragment
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 = prefList.get(key);
if (null == existingPref || hasPriority(status, existingPref.mStatus)) {
- final WordListPreference pref = new WordListPreference(activity, mClientId,
- wordlistId, version, locale, description, status);
+ final WordListPreference pref = new WordListPreference(activity,
+ mDictionaryListInterfaceState, mClientId, wordlistId, version, locale,
+ description, status, filesize);
prefList.put(key, pref);
}
} while (cursor.moveToNext());
diff --git a/java/src/com/android/inputmethod/dictionarypack/LogProblemReporter.java b/java/src/com/android/inputmethod/dictionarypack/LogProblemReporter.java
index c127ad540..c9e128d70 100644
--- a/java/src/com/android/inputmethod/dictionarypack/LogProblemReporter.java
+++ b/java/src/com/android/inputmethod/dictionarypack/LogProblemReporter.java
@@ -28,7 +28,8 @@ final class LogProblemReporter implements ProblemReporter {
TAG = tag;
}
+ @Override
public void report(final Exception e) {
- Log.e(TAG, "Reporting problem : " + e);
+ Log.e(TAG, "Reporting problem", e);
}
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
index 55f545aad..03ed267c3 100644
--- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
@@ -16,13 +16,11 @@
package com.android.inputmethod.dictionarypack;
-import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
-import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
@@ -47,16 +45,16 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
private static final int 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 static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 5;
+ private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6;
// This is the current database version. It should be updated when the database schema
// gets updated. It is passed to the framework constructor of SQLiteOpenHelper, so
// that's what the framework uses to track our database version.
- private static final int METADATA_DATABASE_VERSION = 5;
+ private static final int METADATA_DATABASE_VERSION = 6;
private final static long NOT_A_DOWNLOAD_ID = -1;
public static final String METADATA_TABLE_NAME = "pendingUpdates";
- private static final String CLIENT_TABLE_NAME = "clients";
+ static final String CLIENT_TABLE_NAME = "clients";
public static final String PENDINGID_COLUMN = "pendingid"; // Download Manager ID
public static final String TYPE_COLUMN = "type";
public static final String STATUS_COLUMN = "status";
@@ -75,6 +73,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
private static final String CLIENT_CLIENT_ID_COLUMN = "clientid";
private static final String CLIENT_METADATA_URI_COLUMN = "uri";
+ private static final String CLIENT_METADATA_ADDITIONAL_ID_COLUMN = "additionalid";
private static final String CLIENT_LAST_UPDATE_DATE_COLUMN = "lastupdate";
private static final String CLIENT_PENDINGID_COLUMN = "pendingid"; // Download Manager ID
@@ -130,6 +129,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
"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, "
@@ -284,14 +284,15 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
* @return the string representation of the URI
*/
public static String getMetadataUriAsString(final Context context, final String clientId) {
- SQLiteDatabase defaultDb = getDb(context, null);
- final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
- new String[] { CLIENT_METADATA_URI_COLUMN },
- CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId },
+ SQLiteDatabase defaultDb = MetadataDbHelper.getDb(context, null);
+ final Cursor cursor = defaultDb.query(MetadataDbHelper.CLIENT_TABLE_NAME,
+ new String[] { MetadataDbHelper.CLIENT_METADATA_URI_COLUMN,
+ MetadataDbHelper.CLIENT_METADATA_ADDITIONAL_ID_COLUMN },
+ MetadataDbHelper.CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId },
null, null, null, null);
try {
if (!cursor.moveToFirst()) return null;
- return cursor.getString(0); // Only one column, return it
+ return MetadataUriGetter.getUri(context, cursor.getString(0), cursor.getString(1));
} finally {
cursor.close();
}
@@ -300,7 +301,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
/**
* Update the last metadata update time for all clients using a particular URI.
*
- * All clients using this metadata URI will be indicated as having been updated now.
+ * 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 {@link #getLastUpdateDateForClient(Context, String)},
* until this method is called again.
@@ -309,13 +311,26 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
* @param uri the metadata URI we just downloaded
*/
public static void saveLastUpdateTimeOfUri(final Context context, final String uri) {
- PrivateLog.log("Save last update time of URI : " + uri + " " + System.currentTimeMillis(),
- context);
+ PrivateLog.log("Save last update time of URI : " + uri + " " + System.currentTimeMillis());
final ContentValues values = new ContentValues();
values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis());
final SQLiteDatabase defaultDb = getDb(context, null);
- defaultDb.update(CLIENT_TABLE_NAME, values,
- CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri });
+ final Cursor cursor = MetadataDbHelper.queryClientIds(context);
+ if (null == cursor) return;
+ try {
+ if (!cursor.moveToFirst()) return;
+ do {
+ final String clientId = cursor.getString(0);
+ final String metadataUri =
+ MetadataDbHelper.getMetadataUriAsString(context, clientId);
+ if (metadataUri.equals(uri)) {
+ defaultDb.update(CLIENT_TABLE_NAME, values,
+ CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId });
+ }
+ } while (cursor.moveToNext());
+ } finally {
+ cursor.close();
+ }
}
/**
@@ -730,11 +745,13 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
/**
* Updates information relative to a specific client.
*
- * Updatable information includes only the metadata URI, but may be expanded in the future.
+ * 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.
- * The passed values must also include a non-empty metadata URI in the
- * CLIENT_METADATA_URI_COLUMN column.
+ * 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
@@ -746,10 +763,16 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
// Sanity check the content values
final String valuesClientId = values.getAsString(CLIENT_CLIENT_ID_COLUMN);
final String valuesMetadataUri = values.getAsString(CLIENT_METADATA_URI_COLUMN);
- // Empty string is a valid client ID, but external apps may not configure it.
- // Empty string is a valid metadata URI if the client does not want updates.
- if (TextUtils.isEmpty(valuesClientId) || null == valuesMetadataUri) {
- // We need both these columns to be filled in
+ final String 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
Utils.l("Missing parameter for updateClientInfo");
return;
}
@@ -780,8 +803,9 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
* Register a download ID for a specific metadata URI.
*
* This method should be called when a download for a metadata URI is starting. It will
- * register the download ID for all clients using this metadata URI into the database
- * for later retrieval by {@link #getDownloadRecordsForDownloadId(Context, long)}.
+ * search for all clients using this metadata URI and will register for each of them
+ * the download ID into the database for later retrieval by
+ * {@link #getDownloadRecordsForDownloadId(Context, long)}.
*
* @param context a context for opening databases
* @param uri the metadata URI
@@ -792,8 +816,22 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
final ContentValues values = new ContentValues();
values.put(CLIENT_PENDINGID_COLUMN, downloadId);
final SQLiteDatabase defaultDb = getDb(context, "");
- defaultDb.update(CLIENT_TABLE_NAME, values,
- CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri });
+ final Cursor cursor = MetadataDbHelper.queryClientIds(context);
+ if (null == cursor) return;
+ try {
+ if (!cursor.moveToFirst()) return;
+ do {
+ final String clientId = cursor.getString(0);
+ final String metadataUri =
+ MetadataDbHelper.getMetadataUriAsString(context, clientId);
+ if (metadataUri.equals(uri)) {
+ defaultDb.update(CLIENT_TABLE_NAME, values,
+ CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId });
+ }
+ } while (cursor.moveToNext());
+ } finally {
+ cursor.close();
+ }
}
/**
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataUriGetter.java b/java/src/com/android/inputmethod/dictionarypack/MetadataUriGetter.java
new file mode 100644
index 000000000..ed817658e
--- /dev/null
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataUriGetter.java
@@ -0,0 +1,33 @@
+/*
+ * 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 com.android.inputmethod.dictionarypack;
+
+import android.content.Context;
+
+/**
+ * Helper to get the metadata URI from its base URI and the additional ID, if any.
+ */
+public class MetadataUriGetter {
+ private MetadataUriGetter() {
+ // This helper class is not instantiable.
+ }
+
+ public static String getUri(final Context context, final String baseUri,
+ final String additionalId) {
+ return baseUri;
+ }
+}
diff --git a/java/src/com/android/inputmethod/dictionarypack/PrivateLog.java b/java/src/com/android/inputmethod/dictionarypack/PrivateLog.java
index 8593c1c9b..67dd7b9b7 100644
--- a/java/src/com/android/inputmethod/dictionarypack/PrivateLog.java
+++ b/java/src/com/android/inputmethod/dictionarypack/PrivateLog.java
@@ -16,7 +16,6 @@
package com.android.inputmethod.dictionarypack;
-import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
@@ -24,6 +23,7 @@ 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.
@@ -44,10 +44,10 @@ public class PrivateLog {
+ COLUMN_EVENT + " TEXT);";
private static final SimpleDateFormat sDateFormat =
- new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
+ new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.US);
private static PrivateLog sInstance = new PrivateLog();
- private static DebugHelper mDebugHelper = null;
+ private static DebugHelper sDebugHelper = null;
private PrivateLog() {
}
@@ -55,8 +55,8 @@ public class PrivateLog {
public static synchronized PrivateLog getInstance(final Context context) {
if (!DEBUG) return sInstance;
synchronized(PrivateLog.class) {
- if (sInstance.mDebugHelper == null) {
- sInstance.mDebugHelper = new DebugHelper(context);
+ if (sDebugHelper == null) {
+ sDebugHelper = new DebugHelper(context);
}
return sInstance;
}
@@ -94,16 +94,9 @@ public class PrivateLog {
}
- public static void log(String event, Context context) {
+ public static void log(String event) {
if (!DEBUG) return;
- final SQLiteDatabase l = getInstance(context).mDebugHelper.getWritableDatabase();
- mDebugHelper.insert(l, event);
- }
-
- public static void log(String event, ContentProvider provider) {
- if (!DEBUG) return;
- final SQLiteDatabase l =
- getInstance(provider.getContext()).mDebugHelper.getWritableDatabase();
- mDebugHelper.insert(l, event);
+ final SQLiteDatabase l = sDebugHelper.getWritableDatabase();
+ DebugHelper.insert(l, event);
}
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index 3173e911b..3f917f13f 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -183,7 +183,7 @@ public final class UpdateHandler {
final String clientId = cursor.getString(0);
final String metadataUri =
MetadataDbHelper.getMetadataUriAsString(context, clientId);
- PrivateLog.log("Update for clientId " + Utils.s(clientId), context);
+ PrivateLog.log("Update for clientId " + Utils.s(clientId));
Utils.l("Update for clientId", clientId, " which uses URI ", metadataUri);
uris.add(metadataUri);
} while (cursor.moveToNext());
@@ -211,8 +211,13 @@ public final class UpdateHandler {
*/
private static void updateClientsWithMetadataUri(final Context context,
final boolean updateNow, final String metadataUri) {
- PrivateLog.log("Update for metadata URI " + Utils.s(metadataUri), context);
- final Request metadataRequest = new Request(Uri.parse(metadataUri));
+ PrivateLog.log("Update for metadata URI " + Utils.s(metadataUri));
+ // Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
+ // DownloadManager also stupidly cuts the extension to replace with its own that it
+ // gets from the content-type. We need to circumvent this.
+ final String disambiguator = "#" + System.currentTimeMillis()
+ + com.android.inputmethod.latin.Utils.getVersionName(context) + ".json";
+ final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator));
Utils.l("Request =", metadataRequest);
final Resources res = context.getResources();
@@ -257,7 +262,7 @@ public final class UpdateHandler {
// method will ignore it.
writeMetadataDownloadId(context, metadataUri, downloadId);
}
- PrivateLog.log("Requested download with id " + downloadId, context);
+ PrivateLog.log("Requested download with id " + downloadId);
}
/**
@@ -351,7 +356,13 @@ public final class UpdateHandler {
final int columnUri = cursor.getColumnIndex(DownloadManager.COLUMN_URI);
final int error = cursor.getInt(columnError);
status = cursor.getInt(columnStatus);
- uri = cursor.getString(columnUri);
+ final String uriWithAnchor = cursor.getString(columnUri);
+ int anchorIndex = uriWithAnchor.indexOf('#');
+ if (anchorIndex != -1) {
+ uri = uriWithAnchor.substring(0, anchorIndex);
+ } else {
+ uri = uriWithAnchor;
+ }
if (DownloadManager.STATUS_SUCCESSFUL != status) {
Log.e(TAG, "Permanent failure of download " + downloadId
+ " with error code: " + error);
@@ -404,7 +415,7 @@ public final class UpdateHandler {
/* package */ static void downloadFinished(final Context context, final Intent intent) {
// Get and check the ID of the file that was downloaded
final long fileId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, NOT_AN_ID);
- PrivateLog.log("Download finished with id " + fileId, context);
+ PrivateLog.log("Download finished with id " + fileId);
Utils.l("DownloadFinished with id", fileId);
if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore
@@ -491,7 +502,7 @@ public final class UpdateHandler {
private static void publishUpdateCycleCompletedEvent(final Context context) {
// Even if this is not successful, we have to publish the new state.
- PrivateLog.log("Publishing update cycle completed event", context);
+ PrivateLog.log("Publishing update cycle completed event");
Utils.l("Publishing update cycle completed event");
for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
listener.updateCycleCompleted();
@@ -582,7 +593,7 @@ public final class UpdateHandler {
}
Utils.l("Downloaded metadata :", newMetadata);
- PrivateLog.log("Downloaded metadata\n" + newMetadata, context);
+ PrivateLog.log("Downloaded metadata\n" + newMetadata);
final ActionBatch actions = computeUpgradeTo(context, clientId, newMetadata);
// TODO: Check with UX how we should report to the user
@@ -610,7 +621,7 @@ public final class UpdateHandler {
MetadataDbHelper.DESCRIPTION_COLUMN), "for", downloadRecord.mClientId);
PrivateLog.log("Downloaded a new word list with description : "
+ downloadRecord.mAttributes.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN)
- + " for " + downloadRecord.mClientId, context);
+ + " for " + downloadRecord.mClientId);
final String locale =
downloadRecord.mAttributes.getAsString(MetadataDbHelper.LOCALE_COLUMN);
diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
index 0d923ae01..29015d61b 100644
--- a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
+++ b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
@@ -16,14 +16,15 @@
package com.android.inputmethod.dictionarypack;
-import android.app.Dialog;
import android.content.Context;
import android.content.SharedPreferences;
-import android.preference.DialogPreference;
+import android.preference.Preference;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.Button;
+import android.view.ViewParent;
+import android.widget.ListView;
+import android.widget.TextView;
import com.android.inputmethod.latin.R;
@@ -36,7 +37,7 @@ import java.util.Locale;
* 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 DialogPreference {
+public final class WordListPreference extends Preference {
static final private String TAG = WordListPreference.class.getSimpleName();
// What to display in the "status" field when we receive unknown data as a status from
@@ -59,24 +60,26 @@ public final class WordListPreference extends DialogPreference {
public final int mVersion;
// The status
public int mStatus;
+ // The size of the dictionary file
+ private final int mFilesize;
- // Animation directions
- static final private int ANIMATION_IN = 1;
- static final private int ANIMATION_OUT = 2;
-
- private static Button sLastClickedActionButton = null;
+ private final DictionaryListInterfaceState mInterfaceState;
private final OnWordListPreferenceClick mPreferenceClickHandler =
new OnWordListPreferenceClick();
private final OnActionButtonClick mActionButtonClickHandler =
new OnActionButtonClick();
- public WordListPreference(final Context context, final String clientId, final String wordlistId,
- final int version, final Locale locale, final String description, final int status) {
+ 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);
mContext = context;
+ mInterfaceState = dictionaryListInterfaceState;
mClientId = clientId;
mVersion = version;
mWordlistId = wordlistId;
+ mFilesize = filesize;
setLayoutResource(R.layout.dictionary_line);
@@ -89,12 +92,6 @@ public final class WordListPreference extends DialogPreference {
if (status == mStatus) return;
mStatus = status;
setSummary(getSummary(status));
- // If we are currently displaying the dialog, we should update it, or at least
- // dismiss it.
- final Dialog dialog = getDialog();
- if (null != dialog) {
- dialog.dismiss();
- }
}
private String getSummary(final int status) {
@@ -117,29 +114,31 @@ public final class WordListPreference extends DialogPreference {
}
}
+ // 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
- { R.string.install_dict, ACTION_ENABLE_DICT },
+ { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT },
// MetadataDbHelper.STATUS_DOWNLOADING
- { R.string.cancel_download_dict, ACTION_DISABLE_DICT },
+ { ButtonSwitcher.STATUS_CANCEL, ACTION_DISABLE_DICT },
// MetadataDbHelper.STATUS_INSTALLED
- { R.string.delete_dict, ACTION_DELETE_DICT },
+ { ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT },
// MetadataDbHelper.STATUS_DISABLED
- { R.string.delete_dict, ACTION_DELETE_DICT },
+ { 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.
- { R.string.install_dict, ACTION_ENABLE_DICT }
+ { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT }
};
- private CharSequence getButtonLabel(final int status) {
+ private int getButtonSwitcherStatus(final int status) {
if (status >= sStatusActionList.length) {
Log.e(TAG, "Unknown status " + status);
- return "";
+ return ButtonSwitcher.STATUS_NO_BUTTON;
}
- return mContext.getString(sStatusActionList[status][0]);
+ return sStatusActionList[status][0];
}
private static int getActionIdFromStatusAndMenuEntry(final int status) {
@@ -194,36 +193,70 @@ public final class WordListPreference extends DialogPreference {
protected void onBindView(final View view) {
super.onBindView(view);
((ViewGroup)view).setLayoutTransition(null);
- final Button button = (Button)view.findViewById(R.id.wordlist_button);
- button.setText(getButtonLabel(mStatus));
- button.setVisibility(View.INVISIBLE);
- button.setOnClickListener(mActionButtonClickHandler);
+
+ final DictionaryDownloadProgressBar progressBar =
+ (DictionaryDownloadProgressBar)view.findViewById(R.id.dictionary_line_progress_bar);
+ final TextView status = (TextView)view.findViewById(android.R.id.summary);
+ progressBar.setIds(mClientId, mWordlistId);
+ progressBar.setMax(mFilesize);
+ final boolean showProgressBar = (MetadataDbHelper.STATUS_DOWNLOADING == mStatus);
+ status.setVisibility(showProgressBar ? View.INVISIBLE : View.VISIBLE);
+ progressBar.setVisibility(showProgressBar ? View.VISIBLE : View.INVISIBLE);
+
+ final ButtonSwitcher buttonSwitcher =
+ (ButtonSwitcher)view.findViewById(R.id.wordlist_button_switcher);
+ 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(mActionButtonClickHandler);
view.setOnClickListener(mPreferenceClickHandler);
}
private class OnWordListPreferenceClick implements View.OnClickListener {
@Override
public void onClick(final View v) {
- final Button button = (Button)v.findViewById(R.id.wordlist_button);
- if (null != sLastClickedActionButton) {
- animateButton(sLastClickedActionButton, ANIMATION_OUT);
+ // 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);
+ }
}
- animateButton(button, ANIMATION_IN);
- sLastClickedActionButton = button;
- }
- }
-
- private void animateButton(final Button button, final int direction) {
- final float outerX = ((View)button.getParent()).getWidth();
- final float innerX = button.getX() - button.getTranslationX();
- if (View.INVISIBLE == button.getVisibility()) {
- button.setTranslationX(outerX - innerX);
- button.setVisibility(View.VISIBLE);
- }
- if (ANIMATION_IN == direction) {
- button.animate().translationX(0);
- } else {
- button.animate().translationX(outerX - innerX);
}
}