mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-04-20 14:19:08 +00:00
Removed code for handling dictionary updates and accounts
This commit is contained in:
parent
7bf52c109a
commit
3263ae00cc
33 changed files with 6 additions and 3633 deletions
|
@ -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"
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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:
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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";
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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";
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
Loading…
Add table
Reference in a new issue