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

@ -18,17 +18,12 @@
coreApp="true" coreApp="true"
package="org.dslul.openboard.inputmethod.latin"> package="org.dslul.openboard.inputmethod.latin">
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PROFILE" /> <uses-permission android:name="android.permission.READ_PROFILE" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.READ_USER_DICTIONARY" /> <uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_USER_DICTIONARY" /> <uses-permission android:name="android.permission.WRITE_USER_DICTIONARY" />
<!-- A signature-protected permission to ask AOSP Keyboard to close the software keyboard. <!-- A signature-protected permission to ask AOSP Keyboard to close the software keyboard.
@ -76,10 +71,6 @@
android:resource="@xml/spellchecker" /> android:resource="@xml/spellchecker" />
</service> </service>
<service android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryService"
android:label="@string/dictionary_service_name">
</service>
<!-- Activities --> <!-- Activities -->
<activity android:name=".setup.SetupActivity" <activity android:name=".setup.SetupActivity"
android:theme="@style/platformActivityTheme" android:theme="@style/platformActivityTheme"
@ -134,14 +125,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name="org.dslul.openboard.inputmethod.dictionarypack.DownloadOverMeteredDialog"
android:theme="@style/platformActivityTheme"
android:label="@string/dictionary_install_over_metered_network_prompt">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<!-- Unexported activity used for tests. --> <!-- Unexported activity used for tests. -->
<activity android:name=".settings.TestFragmentActivity" <activity android:name=".settings.TestFragmentActivity"
android:exported="false" /> android:exported="false" />
@ -162,22 +145,6 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="org.dslul.openboard.inputmethod.dictionarypack.EventHandler">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
<action android:name="android.intent.action.DATE_CHANGED" />
<action android:name="org.dslul.openboard.inputmethod.dictionarypack.aosp.UPDATE_NOW" />
<action android:name="org.dslul.openboard.inputmethod.dictionarypack.aosp.INIT_AND_UPDATE_NOW" />
</intent-filter>
</receiver>
<!-- Broadcast receiver for AccountManager#LOGIN_ACCOUNTS_CHANGED_ACTION. -->
<receiver android:name=".accounts.AccountsChangedReceiver">
<intent-filter>
<action android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED" />
</intent-filter>
</receiver>
<!-- Content providers --> <!-- Content providers -->
<provider android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryProvider" <provider android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryProvider"
android:grantUriPermissions="true" android:grantUriPermissions="true"

View file

@ -88,116 +88,6 @@ public final class ActionBatch {
void execute(final Context context); 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. * 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 // The word list is still downloading. Cancel the download and revert the
// word list status to "available". // 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 = final Collection<WordListInfo> dictFiles =
getDictionaryWordListsForLocale(clientId, locale); getDictionaryWordListsForLocale(clientId, locale);
// TODO: pass clientId to the following function // TODO: pass clientId to the following function
DictionaryService.updateNowIfNotUpdatedInAVeryLongTime(getContext());
if (null != dictFiles && dictFiles.size() > 0) { if (null != dictFiles && dictFiles.size() > 0) {
PrivateLog.log("Returned " + dictFiles.size() + " files"); PrivateLog.log("Returned " + dictFiles.size() + " files");
return new ResourcePathCursor(dictFiles); return new ResourcePathCursor(dictFiles);
@ -364,18 +363,8 @@ public final class DictionaryProvider extends ContentProvider {
final String[] wordListIdArray = final String[] wordListIdArray =
TextUtils.split(wordListId, ID_CATEGORY_SEPARATOR); TextUtils.split(wordListId, ID_CATEGORY_SEPARATOR);
final String wordListCategory; final String wordListCategory;
if (2 == wordListIdArray.length) {
// This is at the category:manual_id format. // This is at the category:manual_id format.
wordListCategory = wordListIdArray[0]; 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;
}
final String wordListLocale = results.getString(localeIndex); final String wordListLocale = results.getString(localeIndex);
final String wordListLocalFilename = results.getString(localFileNameIndex); final String wordListLocalFilename = results.getString(localFileNameIndex);
final String wordListRawChecksum = results.getString(rawChecksumIndex); final String wordListRawChecksum = results.getString(rawChecksumIndex);
@ -403,10 +392,6 @@ public final class DictionaryProvider extends ContentProvider {
if (!f.isFile()) { if (!f.isFile()) {
continue; 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); final WordListInfo currentBestMatch = dicts.get(wordListCategory);
if (null == currentBestMatch if (null == currentBestMatch
@ -457,26 +442,6 @@ public final class DictionaryProvider extends ContentProvider {
} }
final int status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN); final int status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
final int version = wordList.getAsInteger(MetadataDbHelper.VERSION_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; return 0;
} }
@ -512,11 +477,6 @@ public final class DictionaryProvider extends ContentProvider {
} catch (final BadFormatException e) { } catch (final BadFormatException e) {
Log.w(TAG, "Not enough information to insert this dictionary " + values, 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; break;
case DICTIONARY_V1_WHOLE_LIST: case DICTIONARY_V1_WHOLE_LIST:
case DICTIONARY_V1_DICT_INFO: 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. * Preference screen.
*/ */
public final class DictionarySettingsFragment extends PreferenceFragment public final class DictionarySettingsFragment extends PreferenceFragment {
implements UpdateHandler.UpdateEventListener {
private static final String TAG = DictionarySettingsFragment.class.getSimpleName(); private static final String TAG = DictionarySettingsFragment.class.getSimpleName();
static final private String DICT_LIST_ID = "list"; static final private String DICT_LIST_ID = "list";
@ -72,13 +71,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
// never null // never null
private TreeMap<String, WordListPreference> mCurrentPreferenceMap = new TreeMap<>(); 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. * Empty constructor for fragment generation.
*/ */
@ -105,41 +97,12 @@ public final class DictionarySettingsFragment extends PreferenceFragment
setHasOptionsMenu(true); 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 @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
mChangedSettings = false; mChangedSettings = false;
UpdateHandler.registerUpdateEventListener(this);
final Activity activity = getActivity(); final Activity activity = getActivity();
final IntentFilter filter = new IntentFilter(); final IntentFilter filter = new IntentFilter();
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
getActivity().registerReceiver(mConnectivityChangedReceiver, filter);
refreshNetworkState();
new Thread("onResume") { new Thread("onResume") {
@Override @Override
@ -161,8 +124,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
final Activity activity = getActivity(); final Activity activity = getActivity();
UpdateHandler.unregisterUpdateEventListener(this);
activity.unregisterReceiver(mConnectivityChangedReceiver);
if (mChangedSettings) { if (mChangedSettings) {
final Intent newDictBroadcast = final Intent newDictBroadcast =
new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); 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) { private WordListPreference findWordListPreference(final String id) {
final PreferenceGroup prefScreen = getPreferenceScreen(); final PreferenceGroup prefScreen = getPreferenceScreen();
if (null == prefScreen) { if (null == prefScreen) {
@ -220,17 +151,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
return null; 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() { void refreshInterface() {
final Activity activity = getActivity(); final Activity activity = getActivity();
if (null == activity) return; if (null == activity) return;
@ -243,8 +163,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
public void run() { public void run() {
// TODO: display this somewhere // TODO: display this somewhere
// if (0 != lastUpdate) mUpdateNowPreference.setSummary(updateNowSummary); // if (0 != lastUpdate) mUpdateNowPreference.setSummary(updateNowSummary);
refreshNetworkState();
removeAnyDictSettings(prefScreen); removeAnyDictSettings(prefScreen);
int i = 0; int i = 0;
for (Preference preference : prefList) { 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(); final ContentValues defaultMetadataValues = new ContentValues();
defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, ""); defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, "");
defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri); defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri);
defaultMetadataValues.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
db.insert(CLIENT_TABLE_NAME, null, defaultMetadataValues); 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) { public static long getOldestUpdateTime(final Context context) {
SQLiteDatabase defaultDb = getDb(context, null); SQLiteDatabase defaultDb = getDb(context, null);
final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, 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); if (null == result.get(FILESIZE_COLUMN)) result.put(FILESIZE_COLUMN, 0);
// Smallest possible version unless specified // Smallest possible version unless specified
if (null == result.get(VERSION_COLUMN)) result.put(VERSION_COLUMN, 1); 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 // No flags unless specified
if (null == result.get(FLAGS_COLUMN)) result.put(FLAGS_COLUMN, 0); if (null == result.get(FLAGS_COLUMN)) result.put(FLAGS_COLUMN, 0);
return result; 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. * Gets the info about a specific word list.
* *
@ -716,7 +645,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
new String[] new String[]
{ id, { id,
Integer.toString(version), Integer.toString(version),
Integer.toString(UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION) Integer.toString(version)
}, },
null /* groupBy */, null /* groupBy */,
null /* having */, null /* having */,
@ -893,7 +822,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
return; return;
} }
// Default value for a pending ID is NOT_AN_ID // Default value for a pending ID is NOT_AN_ID
values.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
final SQLiteDatabase defaultDb = getDb(context, ""); final SQLiteDatabase defaultDb = getDb(context, "");
if (-1 == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) { if (-1 == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) {
defaultDb.update(CLIENT_TABLE_NAME, 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); 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. * 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 int bestFormatVersion = Integer.MIN_VALUE; // To be sure we can't be inadvertently smaller
for (WordListMetadata wordList : metadata) { for (WordListMetadata wordList : metadata) {
if (id.equals(wordList.mId) if (id.equals(wordList.mId)
&& wordList.mFormatVersion <= UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION
&& wordList.mFormatVersion > bestFormatVersion) { && wordList.mFormatVersion > bestFormatVersion) {
bestWordList = wordList; bestWordList = wordList;
bestFormatVersion = wordList.mFormatVersion; bestFormatVersion = wordList.mFormatVersion;

View file

@ -30,7 +30,7 @@ import java.util.Locale;
*/ */
public class PrivateLog { 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_DATABASE_NAME = "log";
private static final String LOG_TABLE_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 Context context = getContext();
final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
CommonPreferences.disable(prefs, mWordlistId); CommonPreferences.disable(prefs, mWordlistId);
UpdateHandler.markAsUnused(context, mClientId, mWordlistId, mVersion, mStatus);
if (MetadataDbHelper.STATUS_DOWNLOADING == mStatus) { if (MetadataDbHelper.STATUS_DOWNLOADING == mStatus) {
setStatus(MetadataDbHelper.STATUS_AVAILABLE); setStatus(MetadataDbHelper.STATUS_AVAILABLE);
} else if (MetadataDbHelper.STATUS_INSTALLED == mStatus) { } else if (MetadataDbHelper.STATUS_INSTALLED == mStatus) {
@ -185,8 +184,6 @@ public final class WordListPreference extends Preference {
final Context context = getContext(); final Context context = getContext();
final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
CommonPreferences.enable(prefs, mWordlistId); 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) { if (MetadataDbHelper.STATUS_AVAILABLE == mStatus) {
setStatus(MetadataDbHelper.STATUS_DOWNLOADING); setStatus(MetadataDbHelper.STATUS_DOWNLOADING);
} else if (MetadataDbHelper.STATUS_DISABLED == mStatus } else if (MetadataDbHelper.STATUS_DISABLED == mStatus
@ -207,7 +204,6 @@ public final class WordListPreference extends Preference {
final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
CommonPreferences.disable(prefs, mWordlistId); CommonPreferences.disable(prefs, mWordlistId);
setStatus(MetadataDbHelper.STATUS_DELETING); setStatus(MetadataDbHelper.STATUS_DELETING);
UpdateHandler.markAsDeleting(context, mClientId, mWordlistId, mVersion, mStatus);
} }
@Override @Override
@ -215,16 +211,6 @@ public final class WordListPreference extends Preference {
super.onBindView(view); super.onBindView(view);
((ViewGroup)view).setLayoutTransition(null); ((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( final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)view.findViewById(
R.id.wordlist_button_switcher); R.id.wordlist_button_switcher);
// We need to clear the state of the button switcher, because we reuse views; if we didn't // 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.DictionaryPackConstants;
import org.dslul.openboard.inputmethod.dictionarypack.MD5Calculator; 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.common.FileUtils;
import org.dslul.openboard.inputmethod.latin.define.DecoderSpecificConstants; import org.dslul.openboard.inputmethod.latin.define.DecoderSpecificConstants;
import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils; 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( public static boolean reportBrokenFileToDictionaryProvider(
final ContentProviderClient providerClient, final String clientId, final ContentProviderClient providerClient, final String clientId,
final String wordlistId) { 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. * 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; InputStream inputStream = null;
try { try {
inputStream = context.getResources().openRawResource(metadataResourceId); inputStream = context.getResources().openRawResource(metadataResourceId);
UpdateHandler.handleMetadata(context, inputStream, clientId);
} catch (Exception e) { } catch (Exception e) {
Log.w(TAG, "Failed to read metadata.json from resources", e); Log.w(TAG, "Failed to read metadata.json from resources", e);
} finally { } finally {

View file

@ -241,19 +241,6 @@ final public class BinaryDictionaryGetter {
*/ */
public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale, public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale,
final Context context, boolean notifyDictionaryPackForUpdates) { 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 File[] cachedWordLists = getCachedWordLists(locale.toString(), context);
final String mainDictId = DictionaryInfoUtils.getMainDictId(locale); final String mainDictId = DictionaryInfoUtils.getMainDictId(locale);
final DictPackSettings dictPackSettings = new DictPackSettings(context); final DictPackSettings dictPackSettings = new DictPackSettings(context);

View file

@ -32,7 +32,6 @@ import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.InputMethodSubtype;
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants; 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.keyboard.KeyboardLayoutSet;
import org.dslul.openboard.inputmethod.latin.settings.Settings; import org.dslul.openboard.inputmethod.latin.settings.Settings;
import org.dslul.openboard.inputmethod.latin.setup.SetupActivity; import org.dslul.openboard.inputmethod.latin.setup.SetupActivity;
@ -77,12 +76,6 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver {
final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes(); final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes();
richImm.setAdditionalInputMethodSubtypes(additionalSubtypes); richImm.setAdditionalInputMethodSubtypes(additionalSubtypes);
toggleAppIcon(context); 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)) { } else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) {
Log.i(TAG, "Boot has been completed"); Log.i(TAG, "Boot has been completed");
toggleAppIcon(context); 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) { public static void toggleAppIcon(final Context context) {
final int appInfoFlags = context.getApplicationInfo().flags; final int appInfoFlags = context.getApplicationInfo().flags;
final boolean isSystemApp = (appInfoFlags & ApplicationInfo.FLAG_SYSTEM) > 0; 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; 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,26 +58,8 @@ public class UserHistoryDictionary extends ExpandableBinaryDictionary {
@UsedForTesting @UsedForTesting
static String getUserHistoryDictName(final String name, final Locale locale, static String getUserHistoryDictName(final String name, final Locale locale,
@Nullable final File dictFile, @Nullable final String account) { @Nullable final File dictFile, @Nullable final String account) {
if (!ProductionFlags.ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY) {
return getDictName(name, locale, dictFile); 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;
}
// Note: This method is called by {@link DictionaryFacilitator} using Java reflection. // Note: This method is called by {@link DictionaryFacilitator} using Java reflection.
@SuppressWarnings("unused") @SuppressWarnings("unused")

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 { public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = Settings.class.getSimpleName(); private static final String TAG = Settings.class.getSimpleName();
// Settings screens // Settings screens
public static final String SCREEN_ACCOUNTS = "screen_accounts";
public static final String SCREEN_THEME = "screen_theme"; public static final String SCREEN_THEME = "screen_theme";
public static final String SCREEN_DEBUG = "screen_debug"; public static final String SCREEN_DEBUG = "screen_debug";
public static final String SCREEN_GESTURE = "screen_gesture"; public static final String SCREEN_GESTURE = "screen_gesture";

View file

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

View file

@ -25,7 +25,6 @@ import android.util.Log;
import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.InputMethodSubtype;
import org.dslul.openboard.inputmethod.annotations.UsedForTesting; 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.AssetFileAddress;
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter; import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter;
import org.dslul.openboard.inputmethod.latin.R; import org.dslul.openboard.inputmethod.latin.R;
@ -61,8 +60,6 @@ public class DictionaryInfoUtils {
// 6 digits - unicode is limited to 21 bits // 6 digits - unicode is limited to 21 bits
private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6; 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 { public static class DictionaryInfo {
private static final String LOCALE_COLUMN = "locale"; private static final String LOCALE_COLUMN = "locale";
private static final String WORDLISTID_COLUMN = "id"; private static final String WORDLISTID_COLUMN = "id";
@ -207,17 +204,6 @@ public class DictionaryInfoUtils {
return new File(DictionaryInfoUtils.getWordListStagingDirectory(context)).listFiles(); 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. * Returns the category for a given file name.
* *
@ -253,75 +239,6 @@ public class DictionaryInfoUtils {
return absoluteDirectoryName; 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) { public static boolean isMainWordListId(final String id) {
final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR); final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
// An id is supposed to be in format category:locale, so splitting on the 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 // Retrieve files from assets
final Resources resources = context.getResources(); final Resources resources = context.getResources();
final AssetManager assets = resources.getAssets(); 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.dictionarypack.DictionarySettingsFragment;
import org.dslul.openboard.inputmethod.latin.about.AboutPreferences; 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.AdvancedSettingsFragment;
import org.dslul.openboard.inputmethod.latin.settings.AppearanceSettingsFragment; import org.dslul.openboard.inputmethod.latin.settings.AppearanceSettingsFragment;
import org.dslul.openboard.inputmethod.latin.settings.CorrectionSettingsFragment; import org.dslul.openboard.inputmethod.latin.settings.CorrectionSettingsFragment;
@ -42,7 +41,6 @@ public class FragmentUtils {
sLatinImeFragments.add(DictionarySettingsFragment.class.getName()); sLatinImeFragments.add(DictionarySettingsFragment.class.getName());
sLatinImeFragments.add(AboutPreferences.class.getName()); sLatinImeFragments.add(AboutPreferences.class.getName());
sLatinImeFragments.add(PreferencesSettingsFragment.class.getName()); sLatinImeFragments.add(PreferencesSettingsFragment.class.getName());
sLatinImeFragments.add(AccountsSettingsFragment.class.getName());
sLatinImeFragments.add(AppearanceSettingsFragment.class.getName()); sLatinImeFragments.add(AppearanceSettingsFragment.class.getName());
sLatinImeFragments.add(ThemeSettingsFragment.class.getName()); sLatinImeFragments.add(ThemeSettingsFragment.class.getName());
sLatinImeFragments.add(CustomInputStyleSettingsFragment.class.getName()); sLatinImeFragments.add(CustomInputStyleSettingsFragment.class.getName());

View file

@ -22,10 +22,6 @@
android:fragment="org.dslul.openboard.inputmethod.latin.settings.PreferencesSettingsFragment" android:fragment="org.dslul.openboard.inputmethod.latin.settings.PreferencesSettingsFragment"
android:title="@string/settings_screen_preferences" android:title="@string/settings_screen_preferences"
android:key="screen_preferences" /> android:key="screen_preferences" />
<PreferenceScreen
android:fragment="org.dslul.openboard.inputmethod.latin.settings.AccountsSettingsFragment"
android:title="@string/settings_screen_accounts"
android:key="screen_accounts" />
<PreferenceScreen <PreferenceScreen
android:fragment="org.dslul.openboard.inputmethod.latin.settings.AppearanceSettingsFragment" android:fragment="org.dslul.openboard.inputmethod.latin.settings.AppearanceSettingsFragment"
android:title="@string/settings_screen_appearance" android:title="@string/settings_screen_appearance"

View file

@ -1,60 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:latin="http://schemas.android.com/apk/res/org.dslul.openboard.inputmethod.latin"
android:title="@string/settings_screen_accounts">
<!-- This preference is a dummy view of the underlying preference.
This isn't persisted and the summary/title is refreshed by the fragment
after inspecting the underlying account preference. -->
<Preference
android:key="account_switcher"
android:persistent="false"
android:title="@string/switch_accounts"
android:summary="@string/no_accounts_selected"
android:enabled="false" />
<!-- Summary will be set programmatically to reflect the account status -->
<CheckBoxPreference
android:key="pref_enable_cloud_sync"
android:title="@string/cloud_sync_title"
android:defaultValue="false"
android:persistent="true"
android:disableDependentsState="false"
android:enabled="false" />
<!-- This preference (acts like a button) enables the user to initiate an one time sync. -->
<Preference android:key="pref_sync_now"
android:persistent="false"
android:title="@string/sync_now_title"
android:dependency="pref_enable_cloud_sync" />
<!-- This preference (acts like a button) enables the user to clear data from the cloud. -->
<Preference android:key="pref_clear_sync_data"
android:persistent="false"
android:title="@string/clear_sync_data_title"
android:summary="@string/clear_sync_data_summary"
android:dependency="pref_enable_cloud_sync" />
<!-- Title will be set programmatically to embed application name -->
<CheckBoxPreference
android:key="pref_enable_metrics_logging"
android:summary="@string/enable_metrics_logging_summary"
android:defaultValue="true"
android:persistent="true" />
</PreferenceScreen>