Removed code for handling dictionary updates and accounts

This commit is contained in:
dslul 2020-01-17 17:36:39 +01:00
parent 7bf52c109a
commit 3263ae00cc
33 changed files with 6 additions and 3633 deletions

View file

@ -88,116 +88,6 @@ public final class ActionBatch {
void execute(final Context context);
}
/**
* An action that starts downloading an available word list.
*/
public static final class StartDownloadAction implements Action {
static final String TAG = "DictionaryProvider:" + StartDownloadAction.class.getSimpleName();
private final String mClientId;
// The data to download. May not be null.
final WordListMetadata mWordList;
public StartDownloadAction(final String clientId, final WordListMetadata wordList) {
DebugLogUtils.l("New download 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, "UpdateAction with a null parameter!");
return;
}
DebugLogUtils.l("Downloading 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);
final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
if (MetadataDbHelper.STATUS_DOWNLOADING == status) {
// The word list is still downloading. Cancel the download and revert the
// word list status to "available".
manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN));
MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion);
} else if (MetadataDbHelper.STATUS_AVAILABLE != status
&& MetadataDbHelper.STATUS_RETRYING != status) {
// Should never happen
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' : " + status
+ " for an upgrade action. Fall back to download.");
}
// Download it.
DebugLogUtils.l("Upgrade word list, downloading", mWordList.mRemoteFilename);
// This is an upgraded word list: we should download it.
// 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()
+ ApplicationUtils.getVersionName(context) + ".dict";
final Uri uri = Uri.parse(mWordList.mRemoteFilename + disambiguator);
final Request request = new Request(uri);
final Resources res = context.getResources();
request.setAllowedNetworkTypes(Request.NETWORK_WIFI | Request.NETWORK_MOBILE);
request.setTitle(mWordList.mDescription);
request.setNotificationVisibility(Request.VISIBILITY_HIDDEN);
request.setVisibleInDownloadsUi(
res.getBoolean(R.bool.dict_downloads_visible_in_download_UI));
final long downloadId = UpdateHandler.registerDownloadRequest(manager, request, db,
mWordList.mId, mWordList.mVersion);
Log.i(TAG, String.format("Starting the dictionary download with version:"
+ " %d and Url: %s", mWordList.mVersion, uri));
DebugLogUtils.l("Starting download of", uri, "with id", downloadId);
PrivateLog.log("Starting download of " + uri + ", id : " + downloadId);
}
}
/**
* An action that updates the database to reflect the status of a newly installed word list.
*/
public static final class InstallAfterDownloadAction implements Action {
static final String TAG = "DictionaryProvider:"
+ InstallAfterDownloadAction.class.getSimpleName();
private final String mClientId;
// The state to upgrade from. May not be null.
final ContentValues mWordListValues;
public InstallAfterDownloadAction(final String clientId,
final ContentValues wordListValues) {
DebugLogUtils.l("New InstallAfterDownloadAction for client ", clientId, " : ",
wordListValues);
mClientId = clientId;
mWordListValues = wordListValues;
}
@Override
public void execute(final Context context) {
if (null == mWordListValues) {
Log.e(TAG, "InstallAfterDownloadAction with a null parameter!");
return;
}
final int status = mWordListValues.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
if (MetadataDbHelper.STATUS_DOWNLOADING != status) {
final String id = mWordListValues.getAsString(MetadataDbHelper.WORDLISTID_COLUMN);
Log.e(TAG, "Unexpected state of the word list '" + id + "' : " + status
+ " for an InstallAfterDownload action. Bailing out.");
return;
}
DebugLogUtils.l("Setting word list as installed");
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
MetadataDbHelper.markEntryAsFinishedDownloadingAndInstalled(db, mWordListValues);
// Install the downloaded file by un-compressing and moving it to the staging
// directory. Ideally, we should do this before updating the DB, but the
// installDictToStagingFromContentProvider() relies on the db being updated.
final String localeString = mWordListValues.getAsString(MetadataDbHelper.LOCALE_COLUMN);
BinaryDictionaryFileDumper.installDictToStagingFromContentProvider(
LocaleUtils.constructLocaleFromString(localeString), context, false);
}
}
/**
* An action that enables an existing word list.
@ -270,9 +160,6 @@ public final class ActionBatch {
}
// The word list is still downloading. Cancel the download and revert the
// word list status to "available".
final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN));
MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion);
}
}
}

View file

@ -1,36 +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.app.DownloadManager;
/**
* Struct class to encapsulate the result of a completed download.
*/
public class CompletedDownloadInfo {
final String mUri;
final long mDownloadId;
final int mStatus;
public CompletedDownloadInfo(final String uri, final long downloadId, final int status) {
mUri = uri;
mDownloadId = downloadId;
mStatus = status;
}
public boolean wasSuccessful() {
return DownloadManager.STATUS_SUCCESSFUL == mStatus;
}
}

View file

@ -1,173 +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.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 {
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() {
super.onDetachedFromWindow();
mIsCurrentlyAttachedToWindow = false;
updateReporterThreadRunningStatusAccordingToVisibility();
}
private class UpdaterThread extends Thread {
private final static int REPORT_PERIOD = 150; // how often to report progress, in ms
final DownloadManagerWrapper mDownloadManagerWrapper;
final int mId;
public UpdaterThread(final Context context, final int id) {
super();
mDownloadManagerWrapper = new DownloadManagerWrapper(context);
mId = id;
}
@Override
public void run() {
try {
final UpdateHelper updateHelper = new UpdateHelper();
final Query query = new Query().setFilterById(mId);
setIndeterminate(true);
while (!isInterrupted()) {
final Cursor cursor = mDownloadManagerWrapper.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.
}
}
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);
}
}
}
}
}

View file

@ -246,7 +246,6 @@ public final class DictionaryProvider extends ContentProvider {
final Collection<WordListInfo> dictFiles =
getDictionaryWordListsForLocale(clientId, locale);
// TODO: pass clientId to the following function
DictionaryService.updateNowIfNotUpdatedInAVeryLongTime(getContext());
if (null != dictFiles && dictFiles.size() > 0) {
PrivateLog.log("Returned " + dictFiles.size() + " files");
return new ResourcePathCursor(dictFiles);
@ -364,18 +363,8 @@ public final class DictionaryProvider extends ContentProvider {
final String[] wordListIdArray =
TextUtils.split(wordListId, ID_CATEGORY_SEPARATOR);
final String wordListCategory;
if (2 == wordListIdArray.length) {
// This is at the category:manual_id format.
wordListCategory = wordListIdArray[0];
// We don't need to read wordListIdArray[1] here, because it's irrelevant to
// word list selection - it's just a name we use to identify which data file
// is a newer version of which word list. We do however return the full id
// string for each selected word list, so in this sense we are 'using' it.
} else {
// This does not contain a colon, like the old format does. Old-format IDs
// always point to main dictionaries, so we force the main category upon it.
wordListCategory = UpdateHandler.MAIN_DICTIONARY_CATEGORY;
}
// 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);
@ -403,10 +392,6 @@ public final class DictionaryProvider extends ContentProvider {
if (!f.isFile()) {
continue;
}
} else if (MetadataDbHelper.STATUS_AVAILABLE == wordListStatus) {
// The locale is the id for the main dictionary.
UpdateHandler.installIfNeverRequested(context, clientId, wordListId);
continue;
}
final WordListInfo currentBestMatch = dicts.get(wordListCategory);
if (null == currentBestMatch
@ -457,26 +442,6 @@ public final class DictionaryProvider extends ContentProvider {
}
final int status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
final int version = wordList.getAsInteger(MetadataDbHelper.VERSION_COLUMN);
if (MetadataDbHelper.STATUS_DELETING == status) {
UpdateHandler.markAsDeleted(getContext(), clientId, wordlistId, version, status);
return 1;
}
if (MetadataDbHelper.STATUS_INSTALLED == status) {
final String result = uri.getQueryParameter(QUERY_PARAMETER_DELETE_RESULT);
if (QUERY_PARAMETER_FAILURE.equals(result)) {
if (DEBUG) {
Log.d(TAG,
"Dictionary is broken, attempting to retry download & installation.");
}
UpdateHandler.markAsBrokenOrRetrying(getContext(), clientId, wordlistId, version);
}
final String localFilename =
wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
final File f = getContext().getFileStreamPath(localFilename);
// f.delete() returns true if the file was successfully deleted, false otherwise
return f.delete() ? 1 : 0;
}
Log.e(TAG, "Attempt to delete a file whose status is " + status);
return 0;
}
@ -512,11 +477,6 @@ public final class DictionaryProvider extends ContentProvider {
} catch (final BadFormatException e) {
Log.w(TAG, "Not enough information to insert this dictionary " + values, e);
}
// We just received new information about the list of dictionary for this client.
// For all intents and purposes, this is new metadata, so we should publish it
// so that any listeners (like the Settings interface for example) can update
// themselves.
UpdateHandler.publishUpdateMetadataCompleted(getContext(), true);
break;
case DICTIONARY_V1_WHOLE_LIST:
case DICTIONARY_V1_DICT_INFO:

View file

@ -1,274 +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.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryFileDumper;
import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
import java.util.Locale;
import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
/**
* Service that handles background tasks for the dictionary provider.
*
* This service provides the context for the long-running operations done by the
* dictionary provider. Those include:
* - Checking for the last update date and scheduling the next update. This runs every
* day around midnight, upon reception of the DATE_CHANGED_INTENT_ACTION broadcast.
* Every four days, it schedules an update of the metadata with the alarm manager.
* - Issuing the order to update the metadata. This runs every four days, between 0 and
* 6, upon reception of the UPDATE_NOW_INTENT_ACTION broadcast sent by the alarm manager
* as a result of the above action.
* - Handling a download that just ended. These come in two flavors:
* - Metadata is finished downloading. We should check whether there are new dictionaries
* available, and download those that we need that have new versions.
* - A dictionary file finished downloading. We should put the file ready for a client IME
* to access, and mark the current state as such.
*/
public final class DictionaryService extends Service {
private static final String TAG = DictionaryService.class.getSimpleName();
/**
* The package name, to use in the intent actions.
*/
private static final String PACKAGE_NAME = "org.dslul.openboard.inputmethod.latin";
/**
* The action of the date changing, used to schedule a periodic freshness check
*/
private static final String DATE_CHANGED_INTENT_ACTION =
Intent.ACTION_DATE_CHANGED;
/**
* The action of displaying a toast to warn the user an automatic download is starting.
*/
/* package */ static final String SHOW_DOWNLOAD_TOAST_INTENT_ACTION =
PACKAGE_NAME + ".SHOW_DOWNLOAD_TOAST_INTENT_ACTION";
/**
* A locale argument, as a String.
*/
/* package */ static final String LOCALE_INTENT_ARGUMENT = "locale";
/**
* How often, in milliseconds, we want to update the metadata. This is a
* floor value; actually, it may happen several hours later, or even more.
*/
private static final long UPDATE_FREQUENCY_MILLIS = TimeUnit.DAYS.toMillis(4);
/**
* We are waked around midnight, local time. We want to wake between midnight and 6 am,
* roughly. So use a random time between 0 and this delay.
*/
private static final int MAX_ALARM_DELAY_MILLIS = (int)TimeUnit.HOURS.toMillis(6);
/**
* How long we consider a "very long time". If no update took place in this time,
* the content provider will trigger an update in the background.
*/
private static final long VERY_LONG_TIME_MILLIS = TimeUnit.DAYS.toMillis(14);
/**
* After starting a download, how long we wait before considering it may be stuck. After this
* period is elapsed, if the keyboard tries to download again, then we cancel and re-register
* the request; if it's within this time, we just leave it be.
* It's important to note that we do not re-submit the request merely because the time is up.
* This is only to decide whether to cancel the old one and re-requesting when the keyboard
* fires a new request for the same data.
*/
public static final long NO_CANCEL_DOWNLOAD_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(30);
/**
* An executor that serializes tasks given to it.
*/
private ThreadPoolExecutor mExecutor;
private static final int WORKER_THREAD_TIMEOUT_SECONDS = 15;
@Override
public void onCreate() {
// By default, a thread pool executor does not timeout its core threads, so it will
// never kill them when there isn't any work to do any more. That would mean the service
// can never die! By creating it this way and calling allowCoreThreadTimeOut, we allow
// the single thread to time out after WORKER_THREAD_TIMEOUT_SECONDS = 15 seconds, allowing
// the process to be reclaimed by the system any time after that if it's not doing
// anything else.
// Executors#newSingleThreadExecutor creates a ThreadPoolExecutor but it returns the
// superclass ExecutorService which does not have the #allowCoreThreadTimeOut method,
// so we can't use that.
mExecutor = new ThreadPoolExecutor(1 /* corePoolSize */, 1 /* maximumPoolSize */,
WORKER_THREAD_TIMEOUT_SECONDS /* keepAliveTime */,
TimeUnit.SECONDS /* unit for keepAliveTime */,
new LinkedBlockingQueue<Runnable>() /* workQueue */);
mExecutor.allowCoreThreadTimeOut(true);
}
@Override
public void onDestroy() {
}
@Override
public IBinder onBind(Intent intent) {
// This service cannot be bound
return null;
}
/**
* Executes an explicit command.
*
* This is the entry point for arbitrary commands that are executed upon reception of certain
* events that should be executed on the context of this service. The supported commands are:
* - Check last update time and possibly schedule an update of the data for later.
* This is triggered every day, upon reception of the DATE_CHANGED_INTENT_ACTION broadcast.
* - Update data NOW.
* This is normally received upon trigger of the scheduled update.
* - Handle a finished download.
* This executes the actions that must be taken after a file (metadata or dictionary data
* has been downloaded (or failed to download).
* The commands that can be spun an another thread will be executed serially, in order, on
* a worker thread that is created on demand and terminates after a short while if there isn't
* any work left to do.
*/
@Override
public synchronized int onStartCommand(final Intent intent, final int flags,
final int startId) {
final DictionaryService self = this;
if (SHOW_DOWNLOAD_TOAST_INTENT_ACTION.equals(intent.getAction())) {
final String localeString = intent.getStringExtra(LOCALE_INTENT_ARGUMENT);
if (localeString == null) {
Log.e(TAG, "Received " + intent.getAction() + " without locale; skipped");
} else {
// This is a UI action, it can't be run in another thread
showStartDownloadingToast(
this, LocaleUtils.constructLocaleFromString(localeString));
}
} else {
// If it's a command that does not require UI, arrange for the work to be done on a
// separate thread, so that we can return right away. The executor will spawn a thread
// if necessary, or reuse a thread that has become idle as appropriate.
// DATE_CHANGED or UPDATE_NOW are examples of commands that can be done on another
// thread.
mExecutor.submit(new Runnable() {
@Override
public void run() {
dispatchBroadcast(self, intent);
// Since calls to onStartCommand are serialized, the submissions to the executor
// are serialized. That means we are guaranteed to call the stopSelfResult()
// in the same order that we got them, so we don't need to take care of the
// order.
stopSelfResult(startId);
}
});
}
return Service.START_REDELIVER_INTENT;
}
static void dispatchBroadcast(final Context context, final Intent intent) {
final String action = intent.getAction();
if (DATE_CHANGED_INTENT_ACTION.equals(action)) {
// This happens when the date of the device changes. This normally happens
// at midnight local time, but it may happen if the user changes the date
// by hand or something similar happens.
checkTimeAndMaybeSetupUpdateAlarm(context);
} else if (DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION.equals(action)) {
// Intent to trigger an update now.
UpdateHandler.tryUpdate(context);
} else if (DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION.equals(action)) {
// Initialize the client Db.
final String mClientId = context.getString(R.string.dictionary_pack_client_id);
BinaryDictionaryFileDumper.initializeClientRecordHelper(context, mClientId);
// Updates the metadata and the download the dictionaries.
UpdateHandler.tryUpdate(context);
} else {
UpdateHandler.downloadFinished(context, intent);
}
}
/**
* Setups an alarm to check for updates if an update is due.
*/
private static void checkTimeAndMaybeSetupUpdateAlarm(final Context context) {
// Of all clients, if the one that hasn't been updated for the longest
// is still more recent than UPDATE_FREQUENCY_MILLIS, do nothing.
if (!isLastUpdateAtLeastThisOld(context, UPDATE_FREQUENCY_MILLIS)) return;
PrivateLog.log("Date changed - registering alarm");
AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
// Best effort to wake between midnight and MAX_ALARM_DELAY_MILLIS in the morning.
// It doesn't matter too much if this is very inexact.
final long now = System.currentTimeMillis();
final long alarmTime = now + new Random().nextInt(MAX_ALARM_DELAY_MILLIS);
final Intent updateIntent = new Intent(DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION);
final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
updateIntent, PendingIntent.FLAG_CANCEL_CURRENT);
// We set the alarm in the type that doesn't forcefully wake the device
// from sleep, but fires the next time the device actually wakes for any
// other reason.
if (null != alarmManager) alarmManager.set(AlarmManager.RTC, alarmTime, pendingIntent);
}
/**
* Utility method to decide whether the last update is older than a certain time.
*
* @return true if at least `time' milliseconds have elapsed since last update, false otherwise.
*/
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);
return lastUpdate + time < now;
}
/**
* Refreshes data if it hasn't been refreshed in a very long time.
*
* This will check the last update time, and if it's been more than VERY_LONG_TIME_MILLIS,
* update metadata now - and possibly take subsequent update actions.
*/
public static void updateNowIfNotUpdatedInAVeryLongTime(final Context context) {
if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME_MILLIS)) return;
UpdateHandler.tryUpdate(context);
}
/**
* Shows a toast informing the user that an automatic dictionary download is starting.
*/
private static void showStartDownloadingToast(final Context context,
@Nonnull final Locale locale) {
final String toastText = String.format(
context.getString(R.string.toast_downloading_suggestions),
locale.getDisplayName());
Toast.makeText(context, toastText, Toast.LENGTH_LONG).show();
}
}

View file

@ -53,8 +53,7 @@ import java.util.TreeMap;
/**
* Preference screen.
*/
public final class DictionarySettingsFragment extends PreferenceFragment
implements UpdateHandler.UpdateEventListener {
public final class DictionarySettingsFragment extends PreferenceFragment {
private static final String TAG = DictionarySettingsFragment.class.getSimpleName();
static final private String DICT_LIST_ID = "list";
@ -72,13 +71,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
// never null
private TreeMap<String, WordListPreference> mCurrentPreferenceMap = new TreeMap<>();
private final BroadcastReceiver mConnectivityChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
refreshNetworkState();
}
};
/**
* Empty constructor for fragment generation.
*/
@ -105,41 +97,12 @@ public final class DictionarySettingsFragment extends PreferenceFragment
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... params) {
return MetadataDbHelper.getMetadataUriAsString(getActivity(), mClientId);
}
@Override
protected void onPostExecute(String metadataUri) {
// We only add the "Refresh" button if we have a non-empty URL to refresh from. If
// the URL is empty, of course we can't refresh so it makes no sense to display
// this.
if (!TextUtils.isEmpty(metadataUri)) {
if (mUpdateNowMenu == null) {
mUpdateNowMenu = menu.add(Menu.NONE, MENU_UPDATE_NOW, 0,
R.string.check_for_updates_now);
mUpdateNowMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
}
refreshNetworkState();
}
}
}.execute();
}
@Override
public void onResume() {
super.onResume();
mChangedSettings = false;
UpdateHandler.registerUpdateEventListener(this);
final Activity activity = getActivity();
final IntentFilter filter = new IntentFilter();
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
getActivity().registerReceiver(mConnectivityChangedReceiver, filter);
refreshNetworkState();
new Thread("onResume") {
@Override
@ -161,8 +124,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
public void onPause() {
super.onPause();
final Activity activity = getActivity();
UpdateHandler.unregisterUpdateEventListener(this);
activity.unregisterReceiver(mConnectivityChangedReceiver);
if (mChangedSettings) {
final Intent newDictBroadcast =
new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
@ -171,36 +132,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
}
}
@Override
public void downloadedMetadata(final boolean succeeded) {
stopLoadingAnimation();
if (!succeeded) return; // If the download failed nothing changed, so no need to refresh
new Thread("refreshInterface") {
@Override
public void run() {
refreshInterface();
}
}.start();
}
@Override
public void wordListDownloadFinished(final String wordListId, final boolean succeeded) {
final WordListPreference pref = findWordListPreference(wordListId);
if (null == pref) return;
// TODO: Report to the user if !succeeded
final Activity activity = getActivity();
if (null == activity) return;
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
// We have to re-read the db in case the description has changed, and to
// find out what state it ended up if the download wasn't successful
// TODO: don't redo everything, only re-read and set this word list status
refreshInterface();
}
});
}
private WordListPreference findWordListPreference(final String id) {
final PreferenceGroup prefScreen = getPreferenceScreen();
if (null == prefScreen) {
@ -220,17 +151,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
return null;
}
@Override
public void updateCycleCompleted() {}
void refreshNetworkState() {
/*
NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
boolean isConnected = null == info ? false : info.isConnected();
*/
if (null != mUpdateNowMenu) mUpdateNowMenu.setEnabled(false);
}
void refreshInterface() {
final Activity activity = getActivity();
if (null == activity) return;
@ -243,8 +163,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
public void run() {
// TODO: display this somewhere
// if (0 != lastUpdate) mUpdateNowPreference.setSummary(updateNowSummary);
refreshNetworkState();
removeAnyDictSettings(prefScreen);
int i = 0;
for (Preference preference : prefList) {
@ -361,80 +279,4 @@ public final class DictionarySettingsFragment extends PreferenceFragment
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case MENU_UPDATE_NOW:
if (View.GONE == mLoadingView.getVisibility()) {
startRefresh();
} else {
cancelRefresh();
}
return true;
}
return false;
}
private void startRefresh() {
startLoadingAnimation();
mChangedSettings = true;
UpdateHandler.registerUpdateEventListener(this);
final Activity activity = getActivity();
new Thread("updateByHand") {
@Override
public void run() {
// We call tryUpdate(), which returns whether we could successfully start an update.
// If we couldn't, we'll never receive the end callback, so we stop the loading
// animation and return to the previous screen.
if (!UpdateHandler.tryUpdate(activity)) {
stopLoadingAnimation();
}
}
}.start();
}
private void cancelRefresh() {
UpdateHandler.unregisterUpdateEventListener(this);
final Context context = getActivity();
new Thread("cancelByHand") {
@Override
public void run() {
UpdateHandler.cancelUpdate(context, mClientId);
stopLoadingAnimation();
}
}.start();
}
private void startLoadingAnimation() {
mLoadingView.setVisibility(View.VISIBLE);
getView().setVisibility(View.GONE);
// We come here when the menu element is pressed so presumably it can't be null. But
// better safe than sorry.
if (null != mUpdateNowMenu) mUpdateNowMenu.setTitle(R.string.cancel);
}
void stopLoadingAnimation() {
final View preferenceView = getView();
final Activity activity = getActivity();
if (null == activity) return;
final View loadingView = mLoadingView;
final MenuItem updateNowMenu = mUpdateNowMenu;
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
loadingView.setVisibility(View.GONE);
preferenceView.setVisibility(View.VISIBLE);
loadingView.startAnimation(AnimationUtils.loadAnimation(
activity, android.R.anim.fade_out));
preferenceView.startAnimation(AnimationUtils.loadAnimation(
activity, android.R.anim.fade_in));
// The menu is created by the framework asynchronously after the activity,
// which means it's possible to have the activity running but the menu not
// created yet - hence the necessity for a null check here.
if (null != updateNowMenu) {
updateNowMenu.setTitle(R.string.check_for_updates_now);
}
}
});
}
}

View file

@ -1,29 +0,0 @@
/*
* Copyright (C) 2014 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;
/**
* A simple container of download ID and download start date.
*/
public class DownloadIdAndStartDate {
public final long mId;
public final long mStartDate;
public DownloadIdAndStartDate(final long id, final long startDate) {
mId = id;
mStartDate = startDate;
}
}

View file

@ -1,110 +0,0 @@
/*
* Copyright (C) 2014 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;
import android.app.DownloadManager.Query;
import android.app.DownloadManager.Request;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.FileNotFoundException;
import javax.annotation.Nullable;
/**
* A class to help with calling DownloadManager methods.
*
* Mostly, the problem here is that most methods from DownloadManager may throw SQL exceptions if
* they can't open the database on disk. We want to avoid crashing in these cases but can't do
* much more, so this class insulates the callers from these. SQLiteException also inherit from
* RuntimeException so they are unchecked :(
* While we're at it, we also insulate callers from the cases where DownloadManager is disabled,
* and getSystemService returns null.
*/
public class DownloadManagerWrapper {
private final static String TAG = DownloadManagerWrapper.class.getSimpleName();
private final DownloadManager mDownloadManager;
public DownloadManagerWrapper(final Context context) {
this((DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE));
}
private DownloadManagerWrapper(final DownloadManager downloadManager) {
mDownloadManager = downloadManager;
}
public void remove(final long... ids) {
try {
if (null != mDownloadManager) {
mDownloadManager.remove(ids);
}
} catch (IllegalArgumentException e) {
// This is expected to happen on boot when the device is encrypted.
} catch (SQLiteException e) {
// We couldn't remove the file from DownloadManager. Apparently, the database can't
// be opened. It may be a problem with file system corruption. In any case, there is
// not much we can do apart from avoiding crashing.
Log.e(TAG, "Can't remove files with ID " + ids + " from download manager", e);
}
}
public ParcelFileDescriptor openDownloadedFile(final long fileId) throws FileNotFoundException {
try {
if (null != mDownloadManager) {
return mDownloadManager.openDownloadedFile(fileId);
}
} catch (IllegalArgumentException e) {
// This is expected to happen on boot when the device is encrypted.
} catch (SQLiteException e) {
Log.e(TAG, "Can't open downloaded file with ID " + fileId, e);
}
// We come here if mDownloadManager is null or if an exception was thrown.
throw new FileNotFoundException();
}
@Nullable
public Cursor query(final Query query) {
try {
if (null != mDownloadManager) {
return mDownloadManager.query(query);
}
} catch (IllegalArgumentException e) {
// This is expected to happen on boot when the device is encrypted.
} catch (SQLiteException e) {
Log.e(TAG, "Can't query the download manager", e);
}
// We come here if mDownloadManager is null or if an exception was thrown.
return null;
}
public long enqueue(final Request request) {
try {
if (null != mDownloadManager) {
return mDownloadManager.enqueue(request);
}
} catch (IllegalArgumentException e) {
// This is expected to happen on boot when the device is encrypted.
} catch (SQLiteException e) {
Log.e(TAG, "Can't enqueue a request with the download manager", e);
}
return 0;
}
}

View file

@ -1,86 +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 android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.Html;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.dslul.openboard.inputmethod.annotations.ExternallyReferenced;
import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
import javax.annotation.Nullable;
/**
* This implements the dialog for asking the user whether it's okay to download dictionaries over
* a metered connection or not (e.g. their mobile data plan).
*/
public final class DownloadOverMeteredDialog extends Activity {
final public static String CLIENT_ID_KEY = "client_id";
final public static String WORDLIST_TO_DOWNLOAD_KEY = "wordlist_to_download";
final public static String SIZE_KEY = "size";
final public static String LOCALE_KEY = "locale";
private String mClientId;
private String mWordListToDownload;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = getIntent();
mClientId = intent.getStringExtra(CLIENT_ID_KEY);
mWordListToDownload = intent.getStringExtra(WORDLIST_TO_DOWNLOAD_KEY);
final String localeString = intent.getStringExtra(LOCALE_KEY);
final long size = intent.getIntExtra(SIZE_KEY, 0);
setContentView(R.layout.download_over_metered);
setTexts(localeString, size);
}
private void setTexts(@Nullable final String localeString, final long size) {
final String promptFormat = getString(R.string.should_download_over_metered_prompt);
final String allowButtonFormat = getString(R.string.download_over_metered);
final String language = (null == localeString) ? ""
: LocaleUtils.constructLocaleFromString(localeString).getDisplayLanguage();
final TextView prompt = (TextView)findViewById(R.id.download_over_metered_prompt);
prompt.setText(Html.fromHtml(String.format(promptFormat, language)));
final Button allowButton = (Button)findViewById(R.id.allow_button);
allowButton.setText(String.format(allowButtonFormat, ((float)size)/(1024*1024)));
}
// This method is externally referenced from layout/download_over_metered.xml using onClick
// attribute of Button.
@ExternallyReferenced
@SuppressWarnings("unused")
public void onClickDeny(final View v) {
UpdateHandler.setDownloadOverMeteredSetting(this, false);
finish();
}
// This method is externally referenced from layout/download_over_metered.xml using onClick
// attribute of Button.
@ExternallyReferenced
@SuppressWarnings("unused")
public void onClickAllow(final View v) {
UpdateHandler.setDownloadOverMeteredSetting(this, true);
UpdateHandler.installIfNeverRequested(this, mClientId, mWordListToDownload);
finish();
}
}

View file

@ -1,37 +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.content.ContentValues;
/**
* Struct class to encapsulate a client ID with content values about a download.
*/
public class DownloadRecord {
public final String mClientId;
// Only word lists have attributes, and the ContentValues should contain the same
// keys as they do for all MetadataDbHelper functions. Since only word lists have
// attributes, a null pointer here means this record represents metadata.
public final ContentValues mAttributes;
public DownloadRecord(final String clientId, final ContentValues attributes) {
mClientId = clientId;
mAttributes = attributes;
}
public boolean isMetadata() {
return null == mAttributes;
}
}

View file

@ -1,46 +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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public final class EventHandler extends BroadcastReceiver {
/**
* Receives a intent broadcast.
*
* We receive every day a broadcast indicating that date changed.
* Then we wait a random amount of time before actually registering
* the download, to avoid concentrating too many accesses around
* midnight in more populated timezones.
* We receive all broadcasts here, so this can be either the DATE_CHANGED broadcast, the
* UPDATE_NOW private broadcast that we receive when the time-randomizing alarm triggers
* for regular update or from applications that want to test the dictionary pack, or a
* broadcast from DownloadManager telling that a download has finished.
* See inside of AndroidManifest.xml to see which events are caught.
* Also @see {@link BroadcastReceiver#onReceive(Context, Intent)}
*
* @param context the context of the application.
* @param intent the intent that was broadcast.
*/
@Override
public void onReceive(final Context context, final Intent intent) {
intent.setClass(context, DictionaryService.class);
context.startService(intent);
}
}

View file

@ -211,7 +211,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
final ContentValues defaultMetadataValues = new ContentValues();
defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, "");
defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri);
defaultMetadataValues.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
db.insert(CLIENT_TABLE_NAME, null, defaultMetadataValues);
}
}
@ -429,31 +428,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
}
}
/**
* Get the metadata download ID for a metadata URI.
*
* This will retrieve the download ID for the metadata file that has the passed URI.
* If this URI is not being downloaded right now, it will return NOT_AN_ID.
*
* @param context a context instance to open the database on
* @param uri the URI to retrieve the metadata download ID of
* @return the download id and start date, or null if the URL is not known
*/
public static DownloadIdAndStartDate getMetadataDownloadIdAndStartDateForURI(
final Context context, final String uri) {
SQLiteDatabase defaultDb = getDb(context, null);
final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
new String[] { CLIENT_PENDINGID_COLUMN, CLIENT_LAST_UPDATE_DATE_COLUMN },
CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri },
null, null, null, null);
try {
if (!cursor.moveToFirst()) return null;
return new DownloadIdAndStartDate(cursor.getInt(0), cursor.getLong(1));
} finally {
cursor.close();
}
}
public static long getOldestUpdateTime(final Context context) {
SQLiteDatabase defaultDb = getDb(context, null);
final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
@ -541,9 +515,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
if (null == result.get(FILESIZE_COLUMN)) result.put(FILESIZE_COLUMN, 0);
// Smallest possible version unless specified
if (null == result.get(VERSION_COLUMN)) result.put(VERSION_COLUMN, 1);
// Assume current format unless specified
if (null == result.get(FORMATVERSION_COLUMN))
result.put(FORMATVERSION_COLUMN, UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION);
// No flags unless specified
if (null == result.get(FLAGS_COLUMN)) result.put(FLAGS_COLUMN, 0);
return result;
@ -656,48 +627,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
}
}
/**
* Given a specific download ID, return records for all pending downloads across all clients.
*
* If several clients use the same metadata URL, we know to only download it once, and
* dispatch the update process across all relevant clients when the download ends. This means
* several clients may share a single download ID if they share a metadata URI.
* The dispatching is done in
* {@link UpdateHandler#downloadFinished(Context, android.content.Intent)}, which
* finds out about the list of relevant clients by calling this method.
*
* @param context a context instance to open the databases
* @param downloadId the download ID to query about
* @return the list of records. Never null, but may be empty.
*/
public static ArrayList<DownloadRecord> getDownloadRecordsForDownloadId(final Context context,
final long downloadId) {
final SQLiteDatabase defaultDb = getDb(context, "");
final ArrayList<DownloadRecord> results = new ArrayList<>();
final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, CLIENT_TABLE_COLUMNS,
null, null, null, null, null);
try {
if (!cursor.moveToFirst()) return results;
final int clientIdIndex = cursor.getColumnIndex(CLIENT_CLIENT_ID_COLUMN);
final int pendingIdColumn = cursor.getColumnIndex(CLIENT_PENDINGID_COLUMN);
do {
final long pendingId = cursor.getInt(pendingIdColumn);
final String clientId = cursor.getString(clientIdIndex);
if (pendingId == downloadId) {
results.add(new DownloadRecord(clientId, null));
}
final ContentValues valuesForThisClient =
getContentValuesByPendingId(getDb(context, clientId), downloadId);
if (null != valuesForThisClient) {
results.add(new DownloadRecord(clientId, valuesForThisClient));
}
} while (cursor.moveToNext());
} finally {
cursor.close();
}
return results;
}
/**
* Gets the info about a specific word list.
*
@ -716,7 +645,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
new String[]
{ id,
Integer.toString(version),
Integer.toString(UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION)
Integer.toString(version)
},
null /* groupBy */,
null /* having */,
@ -893,7 +822,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
return;
}
// Default value for a pending ID is NOT_AN_ID
values.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
final SQLiteDatabase defaultDb = getDb(context, "");
if (-1 == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) {
defaultDb.update(CLIENT_TABLE_NAME, values,
@ -911,42 +839,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
new String[] { CLIENT_CLIENT_ID_COLUMN }, null, null, null, null, null);
}
/**
* 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
* 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
* @param downloadId the download ID
*/
public static void registerMetadataDownloadId(final Context context, final String uri,
final long downloadId) {
final ContentValues values = new ContentValues();
values.put(CLIENT_PENDINGID_COLUMN, downloadId);
values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis());
final SQLiteDatabase defaultDb = getDb(context, "");
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();
}
}
/**
* Marks a downloading entry as having successfully downloaded and being installed.
*

View file

@ -161,7 +161,6 @@ public class MetadataHandler {
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 <= UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION
&& wordList.mFormatVersion > bestFormatVersion) {
bestWordList = wordList;
bestFormatVersion = wordList.mFormatVersion;

View file

@ -30,7 +30,7 @@ import java.util.Locale;
*/
public class PrivateLog {
public static final boolean DEBUG = DictionaryProvider.DEBUG;
public static final boolean DEBUG = false;
private static final String LOG_DATABASE_NAME = "log";
private static final String LOG_TABLE_NAME = "log";

View file

@ -169,7 +169,6 @@ public final class WordListPreference extends Preference {
final Context context = getContext();
final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
CommonPreferences.disable(prefs, mWordlistId);
UpdateHandler.markAsUnused(context, mClientId, mWordlistId, mVersion, mStatus);
if (MetadataDbHelper.STATUS_DOWNLOADING == mStatus) {
setStatus(MetadataDbHelper.STATUS_AVAILABLE);
} else if (MetadataDbHelper.STATUS_INSTALLED == mStatus) {
@ -185,8 +184,6 @@ public final class WordListPreference extends Preference {
final Context context = getContext();
final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
CommonPreferences.enable(prefs, mWordlistId);
// Explicit enabling by the user : allow downloading on metered data connection.
UpdateHandler.markAsUsed(context, mClientId, mWordlistId, mVersion, mStatus, true);
if (MetadataDbHelper.STATUS_AVAILABLE == mStatus) {
setStatus(MetadataDbHelper.STATUS_DOWNLOADING);
} else if (MetadataDbHelper.STATUS_DISABLED == mStatus
@ -207,7 +204,6 @@ public final class WordListPreference extends Preference {
final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
CommonPreferences.disable(prefs, mWordlistId);
setStatus(MetadataDbHelper.STATUS_DELETING);
UpdateHandler.markAsDeleting(context, mClientId, mWordlistId, mVersion, mStatus);
}
@Override
@ -215,16 +211,6 @@ public final class WordListPreference extends Preference {
super.onBindView(view);
((ViewGroup)view).setLayoutTransition(null);
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);
setSummary(getSummary(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);
// We need to clear the state of the button switcher, because we reuse views; if we didn't

View file

@ -29,7 +29,6 @@ import android.util.Log;
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants;
import org.dslul.openboard.inputmethod.dictionarypack.MD5Calculator;
import org.dslul.openboard.inputmethod.dictionarypack.UpdateHandler;
import org.dslul.openboard.inputmethod.latin.common.FileUtils;
import org.dslul.openboard.inputmethod.latin.define.DecoderSpecificConstants;
import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils;
@ -221,152 +220,6 @@ public final class BinaryDictionaryFileDumper {
}
}
/**
* Stages a word list the id of which is passed as an argument. This will write the file
* to the cache file name designated by its id and locale, overwriting it if already present
* and creating it (and its containing directory) if necessary.
*/
private static void installWordListToStaging(final String wordlistId, final String locale,
final String rawChecksum, final ContentProviderClient providerClient,
final Context context) {
final int COMPRESSED_CRYPTED_COMPRESSED = 0;
final int CRYPTED_COMPRESSED = 1;
final int COMPRESSED_CRYPTED = 2;
final int COMPRESSED_ONLY = 3;
final int CRYPTED_ONLY = 4;
final int NONE = 5;
final int MODE_MIN = COMPRESSED_CRYPTED_COMPRESSED;
final int MODE_MAX = NONE;
final String clientId = context.getString(R.string.dictionary_pack_client_id);
final Uri.Builder wordListUriBuilder;
try {
wordListUriBuilder = getContentUriBuilderForType(clientId,
providerClient, QUERY_PATH_DATAFILE, wordlistId /* extraPath */);
} catch (RemoteException e) {
Log.e(TAG, "Can't communicate with the dictionary pack", e);
return;
}
final String finalFileName =
DictionaryInfoUtils.getStagingFileName(wordlistId, locale, context);
String tempFileName;
try {
tempFileName = BinaryDictionaryGetter.getTempFileName(wordlistId, context);
} catch (IOException e) {
Log.e(TAG, "Can't open the temporary file", e);
return;
}
for (int mode = MODE_MIN; mode <= MODE_MAX; ++mode) {
final InputStream originalSourceStream;
InputStream inputStream = null;
InputStream uncompressedStream = null;
InputStream decryptedStream = null;
BufferedInputStream bufferedInputStream = null;
File outputFile = null;
BufferedOutputStream bufferedOutputStream = null;
AssetFileDescriptor afd = null;
final Uri wordListUri = wordListUriBuilder.build();
try {
// Open input.
afd = openAssetFileDescriptor(providerClient, wordListUri);
// If we can't open it at all, don't even try a number of times.
if (null == afd) return;
originalSourceStream = afd.createInputStream();
// Open output.
outputFile = new File(tempFileName);
// Just to be sure, delete the file. This may fail silently, and return false: this
// is the right thing to do, as we just want to continue anyway.
outputFile.delete();
// Get the appropriate decryption method for this try
switch (mode) {
case COMPRESSED_CRYPTED_COMPRESSED:
uncompressedStream =
FileTransforms.getUncompressedStream(originalSourceStream);
decryptedStream = FileTransforms.getDecryptedStream(uncompressedStream);
inputStream = FileTransforms.getUncompressedStream(decryptedStream);
break;
case CRYPTED_COMPRESSED:
decryptedStream = FileTransforms.getDecryptedStream(originalSourceStream);
inputStream = FileTransforms.getUncompressedStream(decryptedStream);
break;
case COMPRESSED_CRYPTED:
uncompressedStream =
FileTransforms.getUncompressedStream(originalSourceStream);
inputStream = FileTransforms.getDecryptedStream(uncompressedStream);
break;
case COMPRESSED_ONLY:
inputStream = FileTransforms.getUncompressedStream(originalSourceStream);
break;
case CRYPTED_ONLY:
inputStream = FileTransforms.getDecryptedStream(originalSourceStream);
break;
case NONE:
inputStream = originalSourceStream;
break;
}
bufferedInputStream = new BufferedInputStream(inputStream);
bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(outputFile));
checkMagicAndCopyFileTo(bufferedInputStream, bufferedOutputStream);
bufferedOutputStream.flush();
bufferedOutputStream.close();
if (SHOULD_VERIFY_CHECKSUM) {
final String actualRawChecksum = MD5Calculator.checksum(
new BufferedInputStream(new FileInputStream(outputFile)));
Log.i(TAG, "Computed checksum for downloaded dictionary. Expected = "
+ rawChecksum + " ; actual = " + actualRawChecksum);
if (!TextUtils.isEmpty(rawChecksum) && !rawChecksum.equals(actualRawChecksum)) {
throw new IOException(
"Could not decode the file correctly : checksum differs");
}
}
// move the output file to the final staging file.
final File finalFile = new File(finalFileName);
if (!FileUtils.renameTo(outputFile, finalFile)) {
Log.e(TAG, String.format("Failed to rename from %s to %s.",
outputFile.getAbsoluteFile(), finalFile.getAbsoluteFile()));
}
wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT,
QUERY_PARAMETER_SUCCESS);
if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) {
Log.e(TAG, "Could not have the dictionary pack delete a word list");
}
Log.d(TAG, "Successfully copied file for wordlist ID " + wordlistId);
// Success! Close files (through the finally{} clause) and return.
return;
} catch (Exception e) {
if (DEBUG) {
Log.e(TAG, "Can't open word list in mode " + mode, e);
}
if (null != outputFile) {
// This may or may not fail. The file may not have been created if the
// exception was thrown before it could be. Hence, both failure and
// success are expected outcomes, so we don't check the return value.
outputFile.delete();
}
// Try the next method.
} finally {
// Ignore exceptions while closing files.
closeAssetFileDescriptorAndReportAnyException(afd);
closeCloseableAndReportAnyException(inputStream);
closeCloseableAndReportAnyException(uncompressedStream);
closeCloseableAndReportAnyException(decryptedStream);
closeCloseableAndReportAnyException(bufferedInputStream);
closeCloseableAndReportAnyException(bufferedOutputStream);
}
}
// We could not copy the file at all. This is very unexpected.
// I'd rather not print the word list ID to the log out of security concerns
Log.e(TAG, "Could not copy a word list. Will not be able to use it.");
// If we can't copy it we should warn the dictionary provider so that it can mark it
// as invalid.
reportBrokenFileToDictionaryProvider(providerClient, clientId, wordlistId);
}
public static boolean reportBrokenFileToDictionaryProvider(
final ContentProviderClient providerClient, final String clientId,
final String wordlistId) {
@ -405,53 +258,6 @@ public final class BinaryDictionaryFileDumper {
}
}
/**
* Queries a content provider for word list data for some locale and stage the returned files
*
* This will query a content provider for word list data for a given locale, and copy the
* files locally so that they can be mmap'ed. This may overwrite previously cached word lists
* with newer versions if a newer version is made available by the content provider.
* @throw FileNotFoundException if the provider returns non-existent data.
* @throw IOException if the provider-returned data could not be read.
*/
public static void installDictToStagingFromContentProvider(final Locale locale,
final Context context, final boolean hasDefaultWordList) {
final ContentProviderClient providerClient;
try {
providerClient = context.getContentResolver().
acquireContentProviderClient(getProviderUriBuilder("").build());
} catch (final SecurityException e) {
Log.e(TAG, "No permission to communicate with the dictionary provider", e);
return;
}
if (null == providerClient) {
Log.e(TAG, "Can't establish communication with the dictionary provider");
return;
}
try {
final List<WordListInfo> idList = getWordListWordListInfos(locale, context,
hasDefaultWordList);
for (WordListInfo id : idList) {
installWordListToStaging(id.mId, id.mLocale, id.mRawChecksum, providerClient,
context);
}
} finally {
providerClient.release();
}
}
/**
* Downloads the dictionary if it was never requested/used.
*
* @param locale locale to download
* @param context the context for resources and providers.
* @param hasDefaultWordList whether the default wordlist exists in the resources.
*/
public static void downloadDictIfNeverRequested(final Locale locale,
final Context context, final boolean hasDefaultWordList) {
getWordListWordListInfos(locale, context, hasDefaultWordList);
}
/**
* Copies the data in an input stream to a target file if the magic number matches.
*
@ -533,7 +339,6 @@ public final class BinaryDictionaryFileDumper {
InputStream inputStream = null;
try {
inputStream = context.getResources().openRawResource(metadataResourceId);
UpdateHandler.handleMetadata(context, inputStream, clientId);
} catch (Exception e) {
Log.w(TAG, "Failed to read metadata.json from resources", e);
} finally {

View file

@ -241,19 +241,6 @@ final public class BinaryDictionaryGetter {
*/
public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale,
final Context context, boolean notifyDictionaryPackForUpdates) {
if (notifyDictionaryPackForUpdates) {
final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable(
context, locale);
// It makes sure that the first time keyboard comes up and the dictionaries are reset,
// the DB is populated with the appropriate values for each locale. Helps in downloading
// the dictionaries when the user enables and switches new languages before the
// DictionaryService runs.
BinaryDictionaryFileDumper.downloadDictIfNeverRequested(
locale, context, hasDefaultWordList);
// Move a staging files to the cache ddirectories if any.
DictionaryInfoUtils.moveStagingFilesIfExists(context);
}
final File[] cachedWordLists = getCachedWordLists(locale.toString(), context);
final String mainDictId = DictionaryInfoUtils.getMainDictId(locale);
final DictPackSettings dictPackSettings = new DictPackSettings(context);

View file

@ -32,7 +32,6 @@ import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants;
import org.dslul.openboard.inputmethod.dictionarypack.DownloadManagerWrapper;
import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
import org.dslul.openboard.inputmethod.latin.setup.SetupActivity;
@ -77,12 +76,6 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver {
final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes();
richImm.setAdditionalInputMethodSubtypes(additionalSubtypes);
toggleAppIcon(context);
// Remove all the previously scheduled downloads. This will also makes sure
// that any erroneously stuck downloads will get cleared. (b/21797386)
removeOldDownloads(context);
// b/21797386
// downloadLatestDictionaries(context);
} else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) {
Log.i(TAG, "Boot has been completed");
toggleAppIcon(context);
@ -110,38 +103,6 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver {
}
}
private void removeOldDownloads(Context context) {
try {
Log.i(TAG, "Removing the old downloads in progress of the previous keyboard version.");
final DownloadManagerWrapper downloadManagerWrapper = new DownloadManagerWrapper(
context);
final DownloadManager.Query q = new DownloadManager.Query();
// Query all the download statuses except the succeeded ones.
q.setFilterByStatus(DownloadManager.STATUS_FAILED
| DownloadManager.STATUS_PAUSED
| DownloadManager.STATUS_PENDING
| DownloadManager.STATUS_RUNNING);
final Cursor c = downloadManagerWrapper.query(q);
if (c != null) {
for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) {
final long downloadId = c
.getLong(c.getColumnIndex(DownloadManager.COLUMN_ID));
downloadManagerWrapper.remove(downloadId);
Log.i(TAG, "Removed the download with Id: " + downloadId);
}
c.close();
}
} catch (Exception e) {
Log.e(TAG, "Exception while removing old downloads.");
}
}
private void downloadLatestDictionaries(Context context) {
final Intent updateIntent = new Intent(
DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION);
context.sendBroadcast(updateIntent);
}
public static void toggleAppIcon(final Context context) {
final int appInfoFlags = context.getApplicationInfo().flags;
final boolean isSystemApp = (appInfoFlags & ApplicationInfo.FLAG_SYSTEM) > 0;

View file

@ -1,75 +0,0 @@
/*
* Copyright (C) 2014 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.latin.accounts;
import androidx.annotation.NonNull;
import javax.annotation.Nullable;
/**
* Handles changes to account used to sign in to the keyboard.
* e.g. account switching/sign-in/sign-out from the keyboard
* user toggling the sync preference.
*/
public class AccountStateChangedListener {
/**
* Called when the current account being used in keyboard is signed out.
*
* @param oldAccount the account that was signed out of.
*/
public static void onAccountSignedOut(@NonNull String oldAccount) {
}
/**
* Called when the user signs-in to the keyboard.
* This may be called when the user switches accounts to sign in with a different account.
*
* @param oldAccount the previous account that was being used for sign-in.
* May be null for a fresh sign-in.
* @param newAccount the account being used for sign-in.
*/
public static void onAccountSignedIn(@Nullable String oldAccount, @NonNull String newAccount) {
}
/**
* Called when the user toggles the sync preference.
*
* @param account the account being used for sync.
* @param syncEnabled indicates whether sync has been enabled or not.
*/
public static void onSyncPreferenceChanged(@Nullable String account, boolean syncEnabled) {
}
/**
* Forces an immediate sync to happen.
* This should only be used for debugging purposes.
*
* @param account the account to use for sync.
*/
public static void forceSync(@Nullable String account) {
}
/**
* Forces an immediate deletion of user's data.
* This should only be used for debugging purposes.
*
* @param account the account to use for sync.
*/
public static void forceDelete(@Nullable String account) {
}
}

View file

@ -1,81 +0,0 @@
/*
* Copyright (C) 2014 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.latin.accounts;
import android.accounts.AccountManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
import org.dslul.openboard.inputmethod.latin.settings.LocalSettingsConstants;
/**
* {@link BroadcastReceiver} for {@link AccountManager#LOGIN_ACCOUNTS_CHANGED_ACTION}.
*/
public class AccountsChangedReceiver extends BroadcastReceiver {
static final String TAG = "AccountsChangedReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (!AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(intent.getAction())) {
Log.w(TAG, "Received unknown broadcast: " + intent);
return;
}
// Ideally the account preference could live in a different preferences file
// that wasn't being backed up and restored, however the preference fragments
// currently only deal with the default shared preferences which is why
// separating this out into a different file is not trivial currently.
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String currentAccount = prefs.getString(
LocalSettingsConstants.PREF_ACCOUNT_NAME, null);
removeUnknownAccountFromPreference(prefs, getAccountsForLogin(context), currentAccount);
}
/**
* Helper method to help test this receiver.
*/
@UsedForTesting
protected String[] getAccountsForLogin(Context context) {
return LoginAccountUtils.getAccountsForLogin(context);
}
/**
* Removes the currentAccount from preferences if it's not found
* in the list of current accounts.
*/
private static void removeUnknownAccountFromPreference(final SharedPreferences prefs,
final String[] accounts, final String currentAccount) {
if (currentAccount == null) {
return;
}
for (final String account : accounts) {
if (TextUtils.equals(currentAccount, account)) {
return;
}
}
Log.i(TAG, "The current account was removed from the system: " + currentAccount);
prefs.edit()
.remove(LocalSettingsConstants.PREF_ACCOUNT_NAME)
.apply();
}
}

View file

@ -1,67 +0,0 @@
/*
* Copyright (C) 2014 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.latin.accounts;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import java.io.IOException;
/**
* Utility class that handles generation/invalidation of auth tokens in the app.
*/
public class AuthUtils {
private final AccountManager mAccountManager;
public AuthUtils(Context context) {
mAccountManager = AccountManager.get(context);
}
/**
* @see AccountManager#invalidateAuthToken(String, String)
*/
public void invalidateAuthToken(final String accountType, final String authToken) {
mAccountManager.invalidateAuthToken(accountType, authToken);
}
/**
* @see AccountManager#getAuthToken(
* Account, String, Bundle, boolean, AccountManagerCallback, Handler)
*/
public AccountManagerFuture<Bundle> getAuthToken(final Account account,
final String authTokenType, final Bundle options, final boolean notifyAuthFailure,
final AccountManagerCallback<Bundle> callback, final Handler handler) {
return mAccountManager.getAuthToken(account, authTokenType, options, notifyAuthFailure,
callback, handler);
}
/**
* @see AccountManager#blockingGetAuthToken(Account, String, boolean)
*/
public String blockingGetAuthToken(final Account account, final String authTokenType,
final boolean notifyAuthFailure) throws OperationCanceledException,
AuthenticatorException, IOException {
return mAccountManager.blockingGetAuthToken(account, authTokenType, notifyAuthFailure);
}
}

View file

@ -1,47 +0,0 @@
/*
* Copyright (C) 2014 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.latin.accounts;
import android.content.Context;
import javax.annotation.Nonnull;
/**
* Utility class for retrieving accounts that may be used for login.
*/
public class LoginAccountUtils {
/**
* This defines the type of account this class deals with.
* This account type is used when listing the accounts available on the device for login.
*/
public static final String ACCOUNT_TYPE = "";
private LoginAccountUtils() {
// This utility class is not publicly instantiable.
}
/**
* Get the accounts available for login.
*
* @return an array of accounts. Empty (never null) if no accounts are available for login.
*/
@Nonnull
@SuppressWarnings("unused")
public static String[] getAccountsForLogin(final Context context) {
return new String[0];
}
}

View file

@ -39,20 +39,4 @@ public final class ProductionFlags {
*/
public static final boolean IS_SPLIT_KEYBOARD_SUPPORTED = true;
/**
* When {@code false}, account sign-in in keyboard is not yet ready to be enabled.
*/
public static final boolean ENABLE_ACCOUNT_SIGN_IN = false;
/**
* When {@code true}, user history dictionary sync feature is ready to be enabled.
*/
public static final boolean ENABLE_USER_HISTORY_DICTIONARY_SYNC =
ENABLE_ACCOUNT_SIGN_IN && false;
/**
* When {@code true}, the IME maintains per account {@link UserHistoryDictionary}.
*/
public static final boolean ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY =
ENABLE_ACCOUNT_SIGN_IN && false;
}

View file

@ -58,25 +58,7 @@ public class UserHistoryDictionary extends ExpandableBinaryDictionary {
@UsedForTesting
static String getUserHistoryDictName(final String name, final Locale locale,
@Nullable final File dictFile, @Nullable final String account) {
if (!ProductionFlags.ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY) {
return getDictName(name, locale, dictFile);
}
return getUserHistoryDictNamePerAccount(name, locale, dictFile, account);
}
/**
* Uses the currently signed in account to determine the dictionary name.
*/
private static String getUserHistoryDictNamePerAccount(final String name, final Locale locale,
@Nullable final File dictFile, @Nullable final String account) {
if (dictFile != null) {
return dictFile.getName();
}
String dictName = name + "." + locale.toString();
if (account != null) {
dictName += "." + account;
}
return dictName;
return getDictName(name, locale, dictFile);
}
// Note: This method is called by {@link DictionaryFacilitator} using Java reflection.

View file

@ -1,508 +0,0 @@
/*
* Copyright (C) 2014 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.latin.settings;
import static org.dslul.openboard.inputmethod.latin.settings.LocalSettingsConstants.PREF_ACCOUNT_NAME;
import static org.dslul.openboard.inputmethod.latin.settings.LocalSettingsConstants.PREF_ENABLE_CLOUD_SYNC;
import android.Manifest;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnShowListener;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.TwoStatePreference;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.widget.ListView;
import android.widget.TextView;
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.accounts.AccountStateChangedListener;
import org.dslul.openboard.inputmethod.latin.accounts.LoginAccountUtils;
import org.dslul.openboard.inputmethod.latin.define.ProductionFlags;
import org.dslul.openboard.inputmethod.latin.permissions.PermissionsUtil;
import org.dslul.openboard.inputmethod.latin.utils.ManagedProfileUtils;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
/**
* "Accounts & Privacy" settings sub screen.
*
* This settings sub screen handles the following preferences:
* <li> Account selection/management for IME </li>
* <li> Sync preferences </li>
* <li> Privacy preferences </li>
*/
public final class AccountsSettingsFragment extends SubScreenFragment {
private static final String PREF_ENABLE_SYNC_NOW = "pref_enable_cloud_sync";
private static final String PREF_SYNC_NOW = "pref_sync_now";
private static final String PREF_CLEAR_SYNC_DATA = "pref_clear_sync_data";
static final String PREF_ACCCOUNT_SWITCHER = "account_switcher";
/**
* Onclick listener for sync now pref.
*/
private final Preference.OnPreferenceClickListener mSyncNowListener =
new SyncNowListener();
/**
* Onclick listener for delete sync pref.
*/
private final Preference.OnPreferenceClickListener mDeleteSyncDataListener =
new DeleteSyncDataListener();
/**
* Onclick listener for enable sync pref.
*/
private final Preference.OnPreferenceClickListener mEnableSyncClickListener =
new EnableSyncClickListener();
/**
* Enable sync checkbox pref.
*/
private TwoStatePreference mEnableSyncPreference;
/**
* Enable sync checkbox pref.
*/
private Preference mSyncNowPreference;
/**
* Clear sync data pref.
*/
private Preference mClearSyncDataPreference;
/**
* Account switcher preference.
*/
private Preference mAccountSwitcher;
/**
* Stores if we are currently detecting a managed profile.
*/
private AtomicBoolean mManagedProfileBeingDetected = new AtomicBoolean(true);
/**
* Stores if we have successfully detected if the device has a managed profile.
*/
private AtomicBoolean mHasManagedProfile = new AtomicBoolean(false);
@Override
public void onCreate(final Bundle icicle) {
super.onCreate(icicle);
addPreferencesFromResource(R.xml.prefs_screen_accounts);
mAccountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER);
mEnableSyncPreference = (TwoStatePreference) findPreference(PREF_ENABLE_SYNC_NOW);
mSyncNowPreference = findPreference(PREF_SYNC_NOW);
mClearSyncDataPreference = findPreference(PREF_CLEAR_SYNC_DATA);
if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) {
final Preference enableMetricsLogging =
findPreference(Settings.PREF_ENABLE_METRICS_LOGGING);
final Resources res = getResources();
if (enableMetricsLogging != null) {
final String enableMetricsLoggingTitle = res.getString(
R.string.enable_metrics_logging, getApplicationName());
enableMetricsLogging.setTitle(enableMetricsLoggingTitle);
}
} else {
removePreference(Settings.PREF_ENABLE_METRICS_LOGGING);
}
if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
removeSyncPreferences();
} else {
// Disable by default till we are sure we can enable this.
disableSyncPreferences();
new ManagedProfileCheckerTask(this).execute();
}
}
/**
* Task to check work profile. If found, it removes the sync prefs. If not,
* it enables them.
*/
private static class ManagedProfileCheckerTask extends AsyncTask<Void, Void, Boolean> {
private final AccountsSettingsFragment mFragment;
private ManagedProfileCheckerTask(final AccountsSettingsFragment fragment) {
mFragment = fragment;
}
@Override
protected void onPreExecute() {
mFragment.mManagedProfileBeingDetected.set(true);
}
@Override
protected Boolean doInBackground(Void... params) {
return ManagedProfileUtils.getInstance().hasWorkProfile(mFragment.getActivity());
}
@Override
protected void onPostExecute(final Boolean hasWorkProfile) {
mFragment.mHasManagedProfile.set(hasWorkProfile);
mFragment.mManagedProfileBeingDetected.set(false);
mFragment.refreshSyncSettingsUI();
}
}
private void enableSyncPreferences(final String[] accountsForLogin,
final String currentAccountName) {
if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
return;
}
mAccountSwitcher.setEnabled(true);
mEnableSyncPreference.setEnabled(true);
mEnableSyncPreference.setOnPreferenceClickListener(mEnableSyncClickListener);
mSyncNowPreference.setEnabled(true);
mSyncNowPreference.setOnPreferenceClickListener(mSyncNowListener);
mClearSyncDataPreference.setEnabled(true);
mClearSyncDataPreference.setOnPreferenceClickListener(mDeleteSyncDataListener);
if (currentAccountName != null) {
mAccountSwitcher.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(final Preference preference) {
if (accountsForLogin.length > 0) {
// TODO: Add addition of account.
createAccountPicker(accountsForLogin, getSignedInAccountName(),
new AccountChangedListener(null)).show();
}
return true;
}
});
}
}
/**
* Two reasons for disable - work profile or no accounts on device.
*/
private void disableSyncPreferences() {
if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
return;
}
mAccountSwitcher.setEnabled(false);
mEnableSyncPreference.setEnabled(false);
mSyncNowPreference.setEnabled(false);
mClearSyncDataPreference.setEnabled(false);
}
/**
* Called only when ProductionFlag is turned off.
*/
private void removeSyncPreferences() {
removePreference(PREF_ACCCOUNT_SWITCHER);
removePreference(PREF_ENABLE_CLOUD_SYNC);
removePreference(PREF_SYNC_NOW);
removePreference(PREF_CLEAR_SYNC_DATA);
}
@Override
public void onResume() {
super.onResume();
refreshSyncSettingsUI();
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
if (TextUtils.equals(key, PREF_ACCOUNT_NAME)) {
refreshSyncSettingsUI();
} else if (TextUtils.equals(key, PREF_ENABLE_CLOUD_SYNC)) {
mEnableSyncPreference = (TwoStatePreference) findPreference(PREF_ENABLE_SYNC_NOW);
final boolean syncEnabled = prefs.getBoolean(PREF_ENABLE_CLOUD_SYNC, false);
if (isSyncEnabled()) {
mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary));
} else {
mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled));
}
AccountStateChangedListener.onSyncPreferenceChanged(getSignedInAccountName(),
syncEnabled);
}
}
/**
* Checks different states like whether account is present or managed profile is present
* and sets the sync settings accordingly.
*/
private void refreshSyncSettingsUI() {
if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
return;
}
boolean hasAccountsPermission = PermissionsUtil.checkAllPermissionsGranted(
getActivity(), Manifest.permission.READ_CONTACTS);
final String[] accountsForLogin = hasAccountsPermission ?
LoginAccountUtils.getAccountsForLogin(getActivity()) : new String[0];
final String currentAccount = hasAccountsPermission ? getSignedInAccountName() : null;
if (hasAccountsPermission && !mManagedProfileBeingDetected.get() &&
!mHasManagedProfile.get() && accountsForLogin.length > 0) {
// Sync can be used by user; enable all preferences.
enableSyncPreferences(accountsForLogin, currentAccount);
} else {
// Sync cannot be used by user; disable all preferences.
disableSyncPreferences();
}
refreshSyncSettingsMessaging(hasAccountsPermission, mManagedProfileBeingDetected.get(),
mHasManagedProfile.get(), accountsForLogin.length > 0,
currentAccount);
}
/**
* @param hasAccountsPermission whether the app has the permission to read accounts.
* @param managedProfileBeingDetected whether we are in process of determining work profile.
* @param hasManagedProfile whether the device has work profile.
* @param hasAccountsForLogin whether the device has enough accounts for login.
* @param currentAccount the account currently selected in the application.
*/
private void refreshSyncSettingsMessaging(boolean hasAccountsPermission,
boolean managedProfileBeingDetected,
boolean hasManagedProfile,
boolean hasAccountsForLogin,
String currentAccount) {
if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
return;
}
if (!hasAccountsPermission) {
mEnableSyncPreference.setChecked(false);
mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled));
mAccountSwitcher.setSummary("");
return;
} else if (managedProfileBeingDetected) {
// If we are determining eligiblity, we show empty summaries.
// Once we have some deterministic result, we set summaries based on different results.
mEnableSyncPreference.setSummary("");
mAccountSwitcher.setSummary("");
} else if (hasManagedProfile) {
mEnableSyncPreference.setSummary(
getString(R.string.cloud_sync_summary_disabled_work_profile));
} else if (!hasAccountsForLogin) {
mEnableSyncPreference.setSummary(getString(R.string.add_account_to_enable_sync));
} else if (isSyncEnabled()) {
mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary));
} else {
mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled));
}
// Set some interdependent settings.
// No account automatically turns off sync.
if (!managedProfileBeingDetected && !hasManagedProfile) {
if (currentAccount != null) {
mAccountSwitcher.setSummary(getString(R.string.account_selected, currentAccount));
} else {
mEnableSyncPreference.setChecked(false);
mAccountSwitcher.setSummary(getString(R.string.no_accounts_selected));
}
}
}
@Nullable
String getSignedInAccountName() {
return getSharedPreferences().getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null);
}
boolean isSyncEnabled() {
return getSharedPreferences().getBoolean(PREF_ENABLE_CLOUD_SYNC, false);
}
/**
* Creates an account picker dialog showing the given accounts in a list and selecting
* the selected account by default. The list of accounts must not be null/empty.
*
* Package-private for testing.
*
* @param accounts list of accounts on the device.
* @param selectedAccount currently selected account
* @param positiveButtonClickListener listener that gets called when positive button is
* clicked
*/
@UsedForTesting
AlertDialog createAccountPicker(final String[] accounts,
final String selectedAccount,
final DialogInterface.OnClickListener positiveButtonClickListener) {
if (accounts == null || accounts.length == 0) {
throw new IllegalArgumentException("List of accounts must not be empty");
}
// See if the currently selected account is in the list.
// If it is, the entry is selected, and a sign-out button is provided.
// If it isn't, select the 0th account by default which will get picked up
// if the user presses OK.
int index = 0;
boolean isSignedIn = false;
for (int i = 0; i < accounts.length; i++) {
if (TextUtils.equals(accounts[i], selectedAccount)) {
index = i;
isSignedIn = true;
break;
}
}
final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(R.string.account_select_title)
.setSingleChoiceItems(accounts, index, null)
.setPositiveButton(R.string.account_select_ok, positiveButtonClickListener)
.setNegativeButton(R.string.account_select_cancel, null);
if (isSignedIn) {
builder.setNeutralButton(R.string.account_select_sign_out, positiveButtonClickListener);
}
return builder.create();
}
/**
* Listener for a account selection changes from the picker.
* Persists/removes the account to/from shared preferences and sets up sync if required.
*/
class AccountChangedListener implements DialogInterface.OnClickListener {
/**
* Represents preference that should be changed based on account chosen.
*/
private TwoStatePreference mDependentPreference;
AccountChangedListener(final TwoStatePreference dependentPreference) {
mDependentPreference = dependentPreference;
}
@Override
public void onClick(final DialogInterface dialog, final int which) {
final String oldAccount = getSignedInAccountName();
switch (which) {
case DialogInterface.BUTTON_POSITIVE: // Signed in
final ListView lv = ((AlertDialog)dialog).getListView();
final String newAccount =
(String) lv.getItemAtPosition(lv.getCheckedItemPosition());
getSharedPreferences()
.edit()
.putString(PREF_ACCOUNT_NAME, newAccount)
.apply();
AccountStateChangedListener.onAccountSignedIn(oldAccount, newAccount);
if (mDependentPreference != null) {
mDependentPreference.setChecked(true);
}
break;
case DialogInterface.BUTTON_NEUTRAL: // Signed out
AccountStateChangedListener.onAccountSignedOut(oldAccount);
getSharedPreferences()
.edit()
.remove(PREF_ACCOUNT_NAME)
.apply();
break;
}
}
}
/**
* Listener that initiates the process of sync in the background.
*/
class SyncNowListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(final Preference preference) {
AccountStateChangedListener.forceSync(getSignedInAccountName());
return true;
}
}
/**
* Listener that initiates the process of deleting user's data from the cloud.
*/
class DeleteSyncDataListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(final Preference preference) {
final AlertDialog confirmationDialog = new AlertDialog.Builder(getActivity())
.setTitle(R.string.clear_sync_data_title)
.setMessage(R.string.clear_sync_data_confirmation)
.setPositiveButton(R.string.clear_sync_data_ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
AccountStateChangedListener.forceDelete(
getSignedInAccountName());
}
}
})
.setNegativeButton(R.string.cloud_sync_cancel, null /* OnClickListener */)
.create();
confirmationDialog.show();
return true;
}
}
/**
* Listens to events when user clicks on "Enable sync" feature.
*/
class EnableSyncClickListener implements OnShowListener, Preference.OnPreferenceClickListener {
// TODO(cvnguyen): Write tests.
@Override
public boolean onPreferenceClick(final Preference preference) {
final TwoStatePreference syncPreference = (TwoStatePreference) preference;
if (syncPreference.isChecked()) {
// Uncheck for now.
syncPreference.setChecked(false);
// Show opt-in.
final AlertDialog optInDialog = new AlertDialog.Builder(getActivity())
.setTitle(R.string.cloud_sync_title)
.setMessage(R.string.cloud_sync_opt_in_text)
.setPositiveButton(R.string.account_select_ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog,
final int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
final Context context = getActivity();
final String[] accountsForLogin =
LoginAccountUtils.getAccountsForLogin(context);
createAccountPicker(accountsForLogin,
getSignedInAccountName(),
new AccountChangedListener(syncPreference))
.show();
}
}
})
.setNegativeButton(R.string.cloud_sync_cancel, null)
.create();
optInDialog.setOnShowListener(this);
optInDialog.show();
}
return true;
}
@Override
public void onShow(DialogInterface dialog) {
TextView messageView = (TextView) ((AlertDialog) dialog).findViewById(
android.R.id.message);
if (messageView != null) {
messageView.setMovementMethod(LinkMovementMethod.getInstance());
}
}
}
}

View file

@ -46,7 +46,6 @@ import javax.annotation.Nonnull;
public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = Settings.class.getSimpleName();
// Settings screens
public static final String SCREEN_ACCOUNTS = "screen_accounts";
public static final String SCREEN_THEME = "screen_theme";
public static final String SCREEN_DEBUG = "screen_debug";
public static final String SCREEN_GESTURE = "screen_gesture";

View file

@ -52,10 +52,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
preferenceScreen.setTitle(
ApplicationUtils.getActivityTitleResId(getActivity(), SettingsActivity.class));
if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) {
final Preference accountsPreference = findPreference(Settings.SCREEN_ACCOUNTS);
preferenceScreen.removePreference(accountsPreference);
}
if (!JniUtils.sHaveGestureLib) {
final Preference gesturePreference = findPreference(Settings.SCREEN_GESTURE);
preferenceScreen.removePreference(gesturePreference);

View file

@ -25,7 +25,6 @@ import android.util.Log;
import android.view.inputmethod.InputMethodSubtype;
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
import org.dslul.openboard.inputmethod.dictionarypack.UpdateHandler;
import org.dslul.openboard.inputmethod.latin.AssetFileAddress;
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter;
import org.dslul.openboard.inputmethod.latin.R;
@ -61,8 +60,6 @@ public class DictionaryInfoUtils {
// 6 digits - unicode is limited to 21 bits
private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6;
private static final String TEMP_DICT_FILE_SUB = UpdateHandler.TEMP_DICT_FILE_SUB;
public static class DictionaryInfo {
private static final String LOCALE_COLUMN = "locale";
private static final String WORDLISTID_COLUMN = "id";
@ -207,17 +204,6 @@ public class DictionaryInfoUtils {
return new File(DictionaryInfoUtils.getWordListStagingDirectory(context)).listFiles();
}
@Nullable
public static File[] getUnusedDictionaryList(final Context context) {
return context.getFilesDir().listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
return !TextUtils.isEmpty(filename) && filename.endsWith(".dict")
&& filename.contains(TEMP_DICT_FILE_SUB);
}
});
}
/**
* Returns the category for a given file name.
*
@ -253,75 +239,6 @@ public class DictionaryInfoUtils {
return absoluteDirectoryName;
}
/**
* Generates a file name for the id and locale passed as an argument.
*
* In the current implementation the file name returned will always be unique for
* any id/locale pair, but please do not expect that the id can be the same for
* different dictionaries with different locales. An id should be unique for any
* dictionary.
* The file name is pretty much an URL-encoded version of the id inside a directory
* named like the locale, except it will also escape characters that look dangerous
* to some file systems.
* @param id the id of the dictionary for which to get a file name
* @param locale the locale for which to get the file name as a string
* @param context the context to use for getting the directory
* @return the name of the file to be created
*/
public static String getCacheFileName(String id, String locale, Context context) {
final String fileName = replaceFileNameDangerousCharacters(id);
return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
}
public static String getStagingFileName(String id, String locale, Context context) {
final String stagingDirectory = getWordListStagingDirectory(context);
// create the directory if it does not exist.
final File directory = new File(stagingDirectory);
if (!directory.exists()) {
if (!directory.mkdirs()) {
Log.e(TAG, "Could not create the staging directory.");
}
}
// e.g. id="main:en_in", locale ="en_IN"
final String fileName = replaceFileNameDangerousCharacters(
locale + TEMP_DICT_FILE_SUB + id);
return stagingDirectory + File.separator + fileName;
}
public static void moveStagingFilesIfExists(Context context) {
final File[] stagingFiles = DictionaryInfoUtils.getStagingDirectoryList(context);
if (stagingFiles != null && stagingFiles.length > 0) {
for (final File stagingFile : stagingFiles) {
final String fileName = stagingFile.getName();
final int index = fileName.indexOf(TEMP_DICT_FILE_SUB);
if (index == -1) {
// This should never happen.
Log.e(TAG, "Staging file does not have ___ substring.");
continue;
}
final String[] localeAndFileId = fileName.split(TEMP_DICT_FILE_SUB);
if (localeAndFileId.length != 2) {
Log.e(TAG, String.format("malformed staging file %s. Deleting.",
stagingFile.getAbsoluteFile()));
stagingFile.delete();
continue;
}
final String locale = localeAndFileId[0];
// already escaped while moving to staging.
final String fileId = localeAndFileId[1];
final String cacheDirectoryForLocale = getCacheDirectoryForLocale(locale, context);
final String cacheFilename = cacheDirectoryForLocale + File.separator + fileId;
final File cacheFile = new File(cacheFilename);
// move the staging file to cache file.
if (!FileUtils.renameTo(stagingFile, cacheFile)) {
Log.e(TAG, String.format("Failed to rename from %s to %s.",
stagingFile.getAbsoluteFile(), cacheFile.getAbsoluteFile()));
}
}
}
}
public static boolean isMainWordListId(final String id) {
final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
// An id is supposed to be in format category:locale, so splitting on the separator
@ -521,25 +438,6 @@ public class DictionaryInfoUtils {
}
}
// Retrieve downloaded dictionaries from the unused dictionaries.
File[] unusedDictionaryList = getUnusedDictionaryList(context);
if (unusedDictionaryList != null) {
for (File dictionaryFile : unusedDictionaryList) {
String fileName = dictionaryFile.getName();
int index = fileName.indexOf(TEMP_DICT_FILE_SUB);
if (index == -1) {
continue;
}
String locale = fileName.substring(0, index);
DictionaryInfo dictionaryInfo = createDictionaryInfoForUnCachedFile(
AssetFileAddress.makeFromFile(dictionaryFile),
LocaleUtils.constructLocaleFromString(locale));
if (dictionaryInfo != null) {
addOrUpdateDictInfo(dictList, dictionaryInfo);
}
}
}
// Retrieve files from assets
final Resources resources = context.getResources();
final AssetManager assets = resources.getAssets();

View file

@ -18,7 +18,6 @@ package org.dslul.openboard.inputmethod.latin.utils;
import org.dslul.openboard.inputmethod.dictionarypack.DictionarySettingsFragment;
import org.dslul.openboard.inputmethod.latin.about.AboutPreferences;
import org.dslul.openboard.inputmethod.latin.settings.AccountsSettingsFragment;
import org.dslul.openboard.inputmethod.latin.settings.AdvancedSettingsFragment;
import org.dslul.openboard.inputmethod.latin.settings.AppearanceSettingsFragment;
import org.dslul.openboard.inputmethod.latin.settings.CorrectionSettingsFragment;
@ -42,7 +41,6 @@ public class FragmentUtils {
sLatinImeFragments.add(DictionarySettingsFragment.class.getName());
sLatinImeFragments.add(AboutPreferences.class.getName());
sLatinImeFragments.add(PreferencesSettingsFragment.class.getName());
sLatinImeFragments.add(AccountsSettingsFragment.class.getName());
sLatinImeFragments.add(AppearanceSettingsFragment.class.getName());
sLatinImeFragments.add(ThemeSettingsFragment.class.getName());
sLatinImeFragments.add(CustomInputStyleSettingsFragment.class.getName());