mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-04-20 22:29:10 +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"
|
||||
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_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.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<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" />
|
||||
|
||||
<!-- A signature-protected permission to ask AOSP Keyboard to close the software keyboard.
|
||||
|
@ -76,10 +71,6 @@
|
|||
android:resource="@xml/spellchecker" />
|
||||
</service>
|
||||
|
||||
<service android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryService"
|
||||
android:label="@string/dictionary_service_name">
|
||||
</service>
|
||||
|
||||
<!-- Activities -->
|
||||
<activity android:name=".setup.SetupActivity"
|
||||
android:theme="@style/platformActivityTheme"
|
||||
|
@ -134,14 +125,6 @@
|
|||
</intent-filter>
|
||||
</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. -->
|
||||
<activity android:name=".settings.TestFragmentActivity"
|
||||
android:exported="false" />
|
||||
|
@ -162,22 +145,6 @@
|
|||
</intent-filter>
|
||||
</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 -->
|
||||
<provider android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryProvider"
|
||||
android:grantUriPermissions="true"
|
||||
|
|
|
@ -88,116 +88,6 @@ public final class ActionBatch {
|
|||
void execute(final Context context);
|
||||
}
|
||||
|
||||
/**
|
||||
* An action that starts downloading an available word list.
|
||||
*/
|
||||
public static final class StartDownloadAction implements Action {
|
||||
static final String TAG = "DictionaryProvider:" + StartDownloadAction.class.getSimpleName();
|
||||
|
||||
private final String mClientId;
|
||||
// The data to download. May not be null.
|
||||
final WordListMetadata mWordList;
|
||||
public StartDownloadAction(final String clientId, final WordListMetadata wordList) {
|
||||
DebugLogUtils.l("New download action for client ", clientId, " : ", wordList);
|
||||
mClientId = clientId;
|
||||
mWordList = wordList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final Context context) {
|
||||
if (null == mWordList) { // This should never happen
|
||||
Log.e(TAG, "UpdateAction with a null parameter!");
|
||||
return;
|
||||
}
|
||||
DebugLogUtils.l("Downloading word list");
|
||||
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
|
||||
mWordList.mId, mWordList.mVersion);
|
||||
final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||
final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
|
||||
if (MetadataDbHelper.STATUS_DOWNLOADING == status) {
|
||||
// The word list is still downloading. Cancel the download and revert the
|
||||
// word list status to "available".
|
||||
manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN));
|
||||
MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion);
|
||||
} else if (MetadataDbHelper.STATUS_AVAILABLE != status
|
||||
&& MetadataDbHelper.STATUS_RETRYING != status) {
|
||||
// Should never happen
|
||||
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' : " + status
|
||||
+ " for an upgrade action. Fall back to download.");
|
||||
}
|
||||
// Download it.
|
||||
DebugLogUtils.l("Upgrade word list, downloading", mWordList.mRemoteFilename);
|
||||
|
||||
// This is an upgraded word list: we should download it.
|
||||
// Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
|
||||
// DownloadManager also stupidly cuts the extension to replace with its own that it
|
||||
// gets from the content-type. We need to circumvent this.
|
||||
final String disambiguator = "#" + System.currentTimeMillis()
|
||||
+ ApplicationUtils.getVersionName(context) + ".dict";
|
||||
final Uri uri = Uri.parse(mWordList.mRemoteFilename + disambiguator);
|
||||
final Request request = new Request(uri);
|
||||
|
||||
final Resources res = context.getResources();
|
||||
request.setAllowedNetworkTypes(Request.NETWORK_WIFI | Request.NETWORK_MOBILE);
|
||||
request.setTitle(mWordList.mDescription);
|
||||
request.setNotificationVisibility(Request.VISIBILITY_HIDDEN);
|
||||
request.setVisibleInDownloadsUi(
|
||||
res.getBoolean(R.bool.dict_downloads_visible_in_download_UI));
|
||||
|
||||
final long downloadId = UpdateHandler.registerDownloadRequest(manager, request, db,
|
||||
mWordList.mId, mWordList.mVersion);
|
||||
Log.i(TAG, String.format("Starting the dictionary download with version:"
|
||||
+ " %d and Url: %s", mWordList.mVersion, uri));
|
||||
DebugLogUtils.l("Starting download of", uri, "with id", downloadId);
|
||||
PrivateLog.log("Starting download of " + uri + ", id : " + downloadId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An action that updates the database to reflect the status of a newly installed word list.
|
||||
*/
|
||||
public static final class InstallAfterDownloadAction implements Action {
|
||||
static final String TAG = "DictionaryProvider:"
|
||||
+ InstallAfterDownloadAction.class.getSimpleName();
|
||||
private final String mClientId;
|
||||
// The state to upgrade from. May not be null.
|
||||
final ContentValues mWordListValues;
|
||||
|
||||
public InstallAfterDownloadAction(final String clientId,
|
||||
final ContentValues wordListValues) {
|
||||
DebugLogUtils.l("New InstallAfterDownloadAction for client ", clientId, " : ",
|
||||
wordListValues);
|
||||
mClientId = clientId;
|
||||
mWordListValues = wordListValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final Context context) {
|
||||
if (null == mWordListValues) {
|
||||
Log.e(TAG, "InstallAfterDownloadAction with a null parameter!");
|
||||
return;
|
||||
}
|
||||
final int status = mWordListValues.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||
if (MetadataDbHelper.STATUS_DOWNLOADING != status) {
|
||||
final String id = mWordListValues.getAsString(MetadataDbHelper.WORDLISTID_COLUMN);
|
||||
Log.e(TAG, "Unexpected state of the word list '" + id + "' : " + status
|
||||
+ " for an InstallAfterDownload action. Bailing out.");
|
||||
return;
|
||||
}
|
||||
|
||||
DebugLogUtils.l("Setting word list as installed");
|
||||
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||
MetadataDbHelper.markEntryAsFinishedDownloadingAndInstalled(db, mWordListValues);
|
||||
|
||||
// Install the downloaded file by un-compressing and moving it to the staging
|
||||
// directory. Ideally, we should do this before updating the DB, but the
|
||||
// installDictToStagingFromContentProvider() relies on the db being updated.
|
||||
final String localeString = mWordListValues.getAsString(MetadataDbHelper.LOCALE_COLUMN);
|
||||
BinaryDictionaryFileDumper.installDictToStagingFromContentProvider(
|
||||
LocaleUtils.constructLocaleFromString(localeString), context, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An action that enables an existing word list.
|
||||
|
@ -270,9 +160,6 @@ public final class ActionBatch {
|
|||
}
|
||||
// The word list is still downloading. Cancel the download and revert the
|
||||
// word list status to "available".
|
||||
final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
|
||||
manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN));
|
||||
MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
getDictionaryWordListsForLocale(clientId, locale);
|
||||
// TODO: pass clientId to the following function
|
||||
DictionaryService.updateNowIfNotUpdatedInAVeryLongTime(getContext());
|
||||
if (null != dictFiles && dictFiles.size() > 0) {
|
||||
PrivateLog.log("Returned " + dictFiles.size() + " files");
|
||||
return new ResourcePathCursor(dictFiles);
|
||||
|
@ -364,18 +363,8 @@ public final class DictionaryProvider extends ContentProvider {
|
|||
final String[] wordListIdArray =
|
||||
TextUtils.split(wordListId, ID_CATEGORY_SEPARATOR);
|
||||
final String wordListCategory;
|
||||
if (2 == wordListIdArray.length) {
|
||||
// This is at the category:manual_id format.
|
||||
wordListCategory = wordListIdArray[0];
|
||||
// We don't need to read wordListIdArray[1] here, because it's irrelevant to
|
||||
// word list selection - it's just a name we use to identify which data file
|
||||
// is a newer version of which word list. We do however return the full id
|
||||
// string for each selected word list, so in this sense we are 'using' it.
|
||||
} else {
|
||||
// This does not contain a colon, like the old format does. Old-format IDs
|
||||
// always point to main dictionaries, so we force the main category upon it.
|
||||
wordListCategory = UpdateHandler.MAIN_DICTIONARY_CATEGORY;
|
||||
}
|
||||
// This is at the category:manual_id format.
|
||||
wordListCategory = wordListIdArray[0];
|
||||
final String wordListLocale = results.getString(localeIndex);
|
||||
final String wordListLocalFilename = results.getString(localFileNameIndex);
|
||||
final String wordListRawChecksum = results.getString(rawChecksumIndex);
|
||||
|
@ -403,10 +392,6 @@ public final class DictionaryProvider extends ContentProvider {
|
|||
if (!f.isFile()) {
|
||||
continue;
|
||||
}
|
||||
} else if (MetadataDbHelper.STATUS_AVAILABLE == wordListStatus) {
|
||||
// The locale is the id for the main dictionary.
|
||||
UpdateHandler.installIfNeverRequested(context, clientId, wordListId);
|
||||
continue;
|
||||
}
|
||||
final WordListInfo currentBestMatch = dicts.get(wordListCategory);
|
||||
if (null == currentBestMatch
|
||||
|
@ -457,26 +442,6 @@ public final class DictionaryProvider extends ContentProvider {
|
|||
}
|
||||
final int status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||
final int version = wordList.getAsInteger(MetadataDbHelper.VERSION_COLUMN);
|
||||
if (MetadataDbHelper.STATUS_DELETING == status) {
|
||||
UpdateHandler.markAsDeleted(getContext(), clientId, wordlistId, version, status);
|
||||
return 1;
|
||||
}
|
||||
if (MetadataDbHelper.STATUS_INSTALLED == status) {
|
||||
final String result = uri.getQueryParameter(QUERY_PARAMETER_DELETE_RESULT);
|
||||
if (QUERY_PARAMETER_FAILURE.equals(result)) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG,
|
||||
"Dictionary is broken, attempting to retry download & installation.");
|
||||
}
|
||||
UpdateHandler.markAsBrokenOrRetrying(getContext(), clientId, wordlistId, version);
|
||||
}
|
||||
final String localFilename =
|
||||
wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
|
||||
final File f = getContext().getFileStreamPath(localFilename);
|
||||
// f.delete() returns true if the file was successfully deleted, false otherwise
|
||||
return f.delete() ? 1 : 0;
|
||||
}
|
||||
Log.e(TAG, "Attempt to delete a file whose status is " + status);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -512,11 +477,6 @@ public final class DictionaryProvider extends ContentProvider {
|
|||
} catch (final BadFormatException e) {
|
||||
Log.w(TAG, "Not enough information to insert this dictionary " + values, e);
|
||||
}
|
||||
// We just received new information about the list of dictionary for this client.
|
||||
// For all intents and purposes, this is new metadata, so we should publish it
|
||||
// so that any listeners (like the Settings interface for example) can update
|
||||
// themselves.
|
||||
UpdateHandler.publishUpdateMetadataCompleted(getContext(), true);
|
||||
break;
|
||||
case DICTIONARY_V1_WHOLE_LIST:
|
||||
case DICTIONARY_V1_DICT_INFO:
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
public final class DictionarySettingsFragment extends PreferenceFragment
|
||||
implements UpdateHandler.UpdateEventListener {
|
||||
public final class DictionarySettingsFragment extends PreferenceFragment {
|
||||
private static final String TAG = DictionarySettingsFragment.class.getSimpleName();
|
||||
|
||||
static final private String DICT_LIST_ID = "list";
|
||||
|
@ -72,13 +71,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
|
|||
// never null
|
||||
private TreeMap<String, WordListPreference> mCurrentPreferenceMap = new TreeMap<>();
|
||||
|
||||
private final BroadcastReceiver mConnectivityChangedReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
refreshNetworkState();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Empty constructor for fragment generation.
|
||||
*/
|
||||
|
@ -105,41 +97,12 @@ public final class DictionarySettingsFragment extends PreferenceFragment
|
|||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
new AsyncTask<Void, Void, String>() {
|
||||
@Override
|
||||
protected String doInBackground(Void... params) {
|
||||
return MetadataDbHelper.getMetadataUriAsString(getActivity(), mClientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String metadataUri) {
|
||||
// We only add the "Refresh" button if we have a non-empty URL to refresh from. If
|
||||
// the URL is empty, of course we can't refresh so it makes no sense to display
|
||||
// this.
|
||||
if (!TextUtils.isEmpty(metadataUri)) {
|
||||
if (mUpdateNowMenu == null) {
|
||||
mUpdateNowMenu = menu.add(Menu.NONE, MENU_UPDATE_NOW, 0,
|
||||
R.string.check_for_updates_now);
|
||||
mUpdateNowMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
}
|
||||
refreshNetworkState();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
mChangedSettings = false;
|
||||
UpdateHandler.registerUpdateEventListener(this);
|
||||
final Activity activity = getActivity();
|
||||
final IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||
getActivity().registerReceiver(mConnectivityChangedReceiver, filter);
|
||||
refreshNetworkState();
|
||||
|
||||
new Thread("onResume") {
|
||||
@Override
|
||||
|
@ -161,8 +124,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
|
|||
public void onPause() {
|
||||
super.onPause();
|
||||
final Activity activity = getActivity();
|
||||
UpdateHandler.unregisterUpdateEventListener(this);
|
||||
activity.unregisterReceiver(mConnectivityChangedReceiver);
|
||||
if (mChangedSettings) {
|
||||
final Intent newDictBroadcast =
|
||||
new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
|
||||
|
@ -171,36 +132,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downloadedMetadata(final boolean succeeded) {
|
||||
stopLoadingAnimation();
|
||||
if (!succeeded) return; // If the download failed nothing changed, so no need to refresh
|
||||
new Thread("refreshInterface") {
|
||||
@Override
|
||||
public void run() {
|
||||
refreshInterface();
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void wordListDownloadFinished(final String wordListId, final boolean succeeded) {
|
||||
final WordListPreference pref = findWordListPreference(wordListId);
|
||||
if (null == pref) return;
|
||||
// TODO: Report to the user if !succeeded
|
||||
final Activity activity = getActivity();
|
||||
if (null == activity) return;
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// We have to re-read the db in case the description has changed, and to
|
||||
// find out what state it ended up if the download wasn't successful
|
||||
// TODO: don't redo everything, only re-read and set this word list status
|
||||
refreshInterface();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private WordListPreference findWordListPreference(final String id) {
|
||||
final PreferenceGroup prefScreen = getPreferenceScreen();
|
||||
if (null == prefScreen) {
|
||||
|
@ -220,17 +151,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
|
|||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCycleCompleted() {}
|
||||
|
||||
void refreshNetworkState() {
|
||||
/*
|
||||
NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
|
||||
boolean isConnected = null == info ? false : info.isConnected();
|
||||
*/
|
||||
if (null != mUpdateNowMenu) mUpdateNowMenu.setEnabled(false);
|
||||
}
|
||||
|
||||
void refreshInterface() {
|
||||
final Activity activity = getActivity();
|
||||
if (null == activity) return;
|
||||
|
@ -243,8 +163,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
|
|||
public void run() {
|
||||
// TODO: display this somewhere
|
||||
// if (0 != lastUpdate) mUpdateNowPreference.setSummary(updateNowSummary);
|
||||
refreshNetworkState();
|
||||
|
||||
removeAnyDictSettings(prefScreen);
|
||||
int i = 0;
|
||||
for (Preference preference : prefList) {
|
||||
|
@ -361,80 +279,4 @@ public final class DictionarySettingsFragment extends PreferenceFragment
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case MENU_UPDATE_NOW:
|
||||
if (View.GONE == mLoadingView.getVisibility()) {
|
||||
startRefresh();
|
||||
} else {
|
||||
cancelRefresh();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void startRefresh() {
|
||||
startLoadingAnimation();
|
||||
mChangedSettings = true;
|
||||
UpdateHandler.registerUpdateEventListener(this);
|
||||
final Activity activity = getActivity();
|
||||
new Thread("updateByHand") {
|
||||
@Override
|
||||
public void run() {
|
||||
// We call tryUpdate(), which returns whether we could successfully start an update.
|
||||
// If we couldn't, we'll never receive the end callback, so we stop the loading
|
||||
// animation and return to the previous screen.
|
||||
if (!UpdateHandler.tryUpdate(activity)) {
|
||||
stopLoadingAnimation();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private void cancelRefresh() {
|
||||
UpdateHandler.unregisterUpdateEventListener(this);
|
||||
final Context context = getActivity();
|
||||
new Thread("cancelByHand") {
|
||||
@Override
|
||||
public void run() {
|
||||
UpdateHandler.cancelUpdate(context, mClientId);
|
||||
stopLoadingAnimation();
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private void startLoadingAnimation() {
|
||||
mLoadingView.setVisibility(View.VISIBLE);
|
||||
getView().setVisibility(View.GONE);
|
||||
// We come here when the menu element is pressed so presumably it can't be null. But
|
||||
// better safe than sorry.
|
||||
if (null != mUpdateNowMenu) mUpdateNowMenu.setTitle(R.string.cancel);
|
||||
}
|
||||
|
||||
void stopLoadingAnimation() {
|
||||
final View preferenceView = getView();
|
||||
final Activity activity = getActivity();
|
||||
if (null == activity) return;
|
||||
final View loadingView = mLoadingView;
|
||||
final MenuItem updateNowMenu = mUpdateNowMenu;
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
loadingView.setVisibility(View.GONE);
|
||||
preferenceView.setVisibility(View.VISIBLE);
|
||||
loadingView.startAnimation(AnimationUtils.loadAnimation(
|
||||
activity, android.R.anim.fade_out));
|
||||
preferenceView.startAnimation(AnimationUtils.loadAnimation(
|
||||
activity, android.R.anim.fade_in));
|
||||
// The menu is created by the framework asynchronously after the activity,
|
||||
// which means it's possible to have the activity running but the menu not
|
||||
// created yet - hence the necessity for a null check here.
|
||||
if (null != updateNowMenu) {
|
||||
updateNowMenu.setTitle(R.string.check_for_updates_now);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, "");
|
||||
defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri);
|
||||
defaultMetadataValues.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
|
||||
db.insert(CLIENT_TABLE_NAME, null, defaultMetadataValues);
|
||||
}
|
||||
}
|
||||
|
@ -429,31 +428,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the metadata download ID for a metadata URI.
|
||||
*
|
||||
* This will retrieve the download ID for the metadata file that has the passed URI.
|
||||
* If this URI is not being downloaded right now, it will return NOT_AN_ID.
|
||||
*
|
||||
* @param context a context instance to open the database on
|
||||
* @param uri the URI to retrieve the metadata download ID of
|
||||
* @return the download id and start date, or null if the URL is not known
|
||||
*/
|
||||
public static DownloadIdAndStartDate getMetadataDownloadIdAndStartDateForURI(
|
||||
final Context context, final String uri) {
|
||||
SQLiteDatabase defaultDb = getDb(context, null);
|
||||
final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
|
||||
new String[] { CLIENT_PENDINGID_COLUMN, CLIENT_LAST_UPDATE_DATE_COLUMN },
|
||||
CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri },
|
||||
null, null, null, null);
|
||||
try {
|
||||
if (!cursor.moveToFirst()) return null;
|
||||
return new DownloadIdAndStartDate(cursor.getInt(0), cursor.getLong(1));
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static long getOldestUpdateTime(final Context context) {
|
||||
SQLiteDatabase defaultDb = getDb(context, null);
|
||||
final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
|
||||
|
@ -541,9 +515,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
|
|||
if (null == result.get(FILESIZE_COLUMN)) result.put(FILESIZE_COLUMN, 0);
|
||||
// Smallest possible version unless specified
|
||||
if (null == result.get(VERSION_COLUMN)) result.put(VERSION_COLUMN, 1);
|
||||
// Assume current format unless specified
|
||||
if (null == result.get(FORMATVERSION_COLUMN))
|
||||
result.put(FORMATVERSION_COLUMN, UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION);
|
||||
// No flags unless specified
|
||||
if (null == result.get(FLAGS_COLUMN)) result.put(FLAGS_COLUMN, 0);
|
||||
return result;
|
||||
|
@ -656,48 +627,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a specific download ID, return records for all pending downloads across all clients.
|
||||
*
|
||||
* If several clients use the same metadata URL, we know to only download it once, and
|
||||
* dispatch the update process across all relevant clients when the download ends. This means
|
||||
* several clients may share a single download ID if they share a metadata URI.
|
||||
* The dispatching is done in
|
||||
* {@link UpdateHandler#downloadFinished(Context, android.content.Intent)}, which
|
||||
* finds out about the list of relevant clients by calling this method.
|
||||
*
|
||||
* @param context a context instance to open the databases
|
||||
* @param downloadId the download ID to query about
|
||||
* @return the list of records. Never null, but may be empty.
|
||||
*/
|
||||
public static ArrayList<DownloadRecord> getDownloadRecordsForDownloadId(final Context context,
|
||||
final long downloadId) {
|
||||
final SQLiteDatabase defaultDb = getDb(context, "");
|
||||
final ArrayList<DownloadRecord> results = new ArrayList<>();
|
||||
final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, CLIENT_TABLE_COLUMNS,
|
||||
null, null, null, null, null);
|
||||
try {
|
||||
if (!cursor.moveToFirst()) return results;
|
||||
final int clientIdIndex = cursor.getColumnIndex(CLIENT_CLIENT_ID_COLUMN);
|
||||
final int pendingIdColumn = cursor.getColumnIndex(CLIENT_PENDINGID_COLUMN);
|
||||
do {
|
||||
final long pendingId = cursor.getInt(pendingIdColumn);
|
||||
final String clientId = cursor.getString(clientIdIndex);
|
||||
if (pendingId == downloadId) {
|
||||
results.add(new DownloadRecord(clientId, null));
|
||||
}
|
||||
final ContentValues valuesForThisClient =
|
||||
getContentValuesByPendingId(getDb(context, clientId), downloadId);
|
||||
if (null != valuesForThisClient) {
|
||||
results.add(new DownloadRecord(clientId, valuesForThisClient));
|
||||
}
|
||||
} while (cursor.moveToNext());
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the info about a specific word list.
|
||||
*
|
||||
|
@ -716,7 +645,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
|
|||
new String[]
|
||||
{ id,
|
||||
Integer.toString(version),
|
||||
Integer.toString(UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION)
|
||||
Integer.toString(version)
|
||||
},
|
||||
null /* groupBy */,
|
||||
null /* having */,
|
||||
|
@ -893,7 +822,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
|
|||
return;
|
||||
}
|
||||
// Default value for a pending ID is NOT_AN_ID
|
||||
values.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
|
||||
final SQLiteDatabase defaultDb = getDb(context, "");
|
||||
if (-1 == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) {
|
||||
defaultDb.update(CLIENT_TABLE_NAME, values,
|
||||
|
@ -911,42 +839,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
|
|||
new String[] { CLIENT_CLIENT_ID_COLUMN }, null, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a download ID for a specific metadata URI.
|
||||
*
|
||||
* This method should be called when a download for a metadata URI is starting. It will
|
||||
* search for all clients using this metadata URI and will register for each of them
|
||||
* the download ID into the database for later retrieval by
|
||||
* {@link #getDownloadRecordsForDownloadId(Context, long)}.
|
||||
*
|
||||
* @param context a context for opening databases
|
||||
* @param uri the metadata URI
|
||||
* @param downloadId the download ID
|
||||
*/
|
||||
public static void registerMetadataDownloadId(final Context context, final String uri,
|
||||
final long downloadId) {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(CLIENT_PENDINGID_COLUMN, downloadId);
|
||||
values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis());
|
||||
final SQLiteDatabase defaultDb = getDb(context, "");
|
||||
final Cursor cursor = MetadataDbHelper.queryClientIds(context);
|
||||
if (null == cursor) return;
|
||||
try {
|
||||
if (!cursor.moveToFirst()) return;
|
||||
do {
|
||||
final String clientId = cursor.getString(0);
|
||||
final String metadataUri =
|
||||
MetadataDbHelper.getMetadataUriAsString(context, clientId);
|
||||
if (metadataUri.equals(uri)) {
|
||||
defaultDb.update(CLIENT_TABLE_NAME, values,
|
||||
CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId });
|
||||
}
|
||||
} while (cursor.moveToNext());
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a downloading entry as having successfully downloaded and being installed.
|
||||
*
|
||||
|
|
|
@ -161,7 +161,6 @@ public class MetadataHandler {
|
|||
int bestFormatVersion = Integer.MIN_VALUE; // To be sure we can't be inadvertently smaller
|
||||
for (WordListMetadata wordList : metadata) {
|
||||
if (id.equals(wordList.mId)
|
||||
&& wordList.mFormatVersion <= UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION
|
||||
&& wordList.mFormatVersion > bestFormatVersion) {
|
||||
bestWordList = wordList;
|
||||
bestFormatVersion = wordList.mFormatVersion;
|
||||
|
|
|
@ -30,7 +30,7 @@ import java.util.Locale;
|
|||
*/
|
||||
public class PrivateLog {
|
||||
|
||||
public static final boolean DEBUG = DictionaryProvider.DEBUG;
|
||||
public static final boolean DEBUG = false;
|
||||
|
||||
private static final String LOG_DATABASE_NAME = "log";
|
||||
private static final String LOG_TABLE_NAME = "log";
|
||||
|
|
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 SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
|
||||
CommonPreferences.disable(prefs, mWordlistId);
|
||||
UpdateHandler.markAsUnused(context, mClientId, mWordlistId, mVersion, mStatus);
|
||||
if (MetadataDbHelper.STATUS_DOWNLOADING == mStatus) {
|
||||
setStatus(MetadataDbHelper.STATUS_AVAILABLE);
|
||||
} else if (MetadataDbHelper.STATUS_INSTALLED == mStatus) {
|
||||
|
@ -185,8 +184,6 @@ public final class WordListPreference extends Preference {
|
|||
final Context context = getContext();
|
||||
final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
|
||||
CommonPreferences.enable(prefs, mWordlistId);
|
||||
// Explicit enabling by the user : allow downloading on metered data connection.
|
||||
UpdateHandler.markAsUsed(context, mClientId, mWordlistId, mVersion, mStatus, true);
|
||||
if (MetadataDbHelper.STATUS_AVAILABLE == mStatus) {
|
||||
setStatus(MetadataDbHelper.STATUS_DOWNLOADING);
|
||||
} else if (MetadataDbHelper.STATUS_DISABLED == mStatus
|
||||
|
@ -207,7 +204,6 @@ public final class WordListPreference extends Preference {
|
|||
final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
|
||||
CommonPreferences.disable(prefs, mWordlistId);
|
||||
setStatus(MetadataDbHelper.STATUS_DELETING);
|
||||
UpdateHandler.markAsDeleting(context, mClientId, mWordlistId, mVersion, mStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -215,16 +211,6 @@ public final class WordListPreference extends Preference {
|
|||
super.onBindView(view);
|
||||
((ViewGroup)view).setLayoutTransition(null);
|
||||
|
||||
final DictionaryDownloadProgressBar progressBar =
|
||||
(DictionaryDownloadProgressBar)view.findViewById(R.id.dictionary_line_progress_bar);
|
||||
final TextView status = (TextView)view.findViewById(android.R.id.summary);
|
||||
progressBar.setIds(mClientId, mWordlistId);
|
||||
progressBar.setMax(mFilesize);
|
||||
final boolean showProgressBar = (MetadataDbHelper.STATUS_DOWNLOADING == mStatus);
|
||||
setSummary(getSummary(mStatus));
|
||||
status.setVisibility(showProgressBar ? View.INVISIBLE : View.VISIBLE);
|
||||
progressBar.setVisibility(showProgressBar ? View.VISIBLE : View.INVISIBLE);
|
||||
|
||||
final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)view.findViewById(
|
||||
R.id.wordlist_button_switcher);
|
||||
// We need to clear the state of the button switcher, because we reuse views; if we didn't
|
||||
|
|
|
@ -29,7 +29,6 @@ import android.util.Log;
|
|||
|
||||
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants;
|
||||
import org.dslul.openboard.inputmethod.dictionarypack.MD5Calculator;
|
||||
import org.dslul.openboard.inputmethod.dictionarypack.UpdateHandler;
|
||||
import org.dslul.openboard.inputmethod.latin.common.FileUtils;
|
||||
import org.dslul.openboard.inputmethod.latin.define.DecoderSpecificConstants;
|
||||
import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils;
|
||||
|
@ -221,152 +220,6 @@ public final class BinaryDictionaryFileDumper {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stages a word list the id of which is passed as an argument. This will write the file
|
||||
* to the cache file name designated by its id and locale, overwriting it if already present
|
||||
* and creating it (and its containing directory) if necessary.
|
||||
*/
|
||||
private static void installWordListToStaging(final String wordlistId, final String locale,
|
||||
final String rawChecksum, final ContentProviderClient providerClient,
|
||||
final Context context) {
|
||||
final int COMPRESSED_CRYPTED_COMPRESSED = 0;
|
||||
final int CRYPTED_COMPRESSED = 1;
|
||||
final int COMPRESSED_CRYPTED = 2;
|
||||
final int COMPRESSED_ONLY = 3;
|
||||
final int CRYPTED_ONLY = 4;
|
||||
final int NONE = 5;
|
||||
final int MODE_MIN = COMPRESSED_CRYPTED_COMPRESSED;
|
||||
final int MODE_MAX = NONE;
|
||||
|
||||
final String clientId = context.getString(R.string.dictionary_pack_client_id);
|
||||
final Uri.Builder wordListUriBuilder;
|
||||
try {
|
||||
wordListUriBuilder = getContentUriBuilderForType(clientId,
|
||||
providerClient, QUERY_PATH_DATAFILE, wordlistId /* extraPath */);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Can't communicate with the dictionary pack", e);
|
||||
return;
|
||||
}
|
||||
final String finalFileName =
|
||||
DictionaryInfoUtils.getStagingFileName(wordlistId, locale, context);
|
||||
String tempFileName;
|
||||
try {
|
||||
tempFileName = BinaryDictionaryGetter.getTempFileName(wordlistId, context);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Can't open the temporary file", e);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int mode = MODE_MIN; mode <= MODE_MAX; ++mode) {
|
||||
final InputStream originalSourceStream;
|
||||
InputStream inputStream = null;
|
||||
InputStream uncompressedStream = null;
|
||||
InputStream decryptedStream = null;
|
||||
BufferedInputStream bufferedInputStream = null;
|
||||
File outputFile = null;
|
||||
BufferedOutputStream bufferedOutputStream = null;
|
||||
AssetFileDescriptor afd = null;
|
||||
final Uri wordListUri = wordListUriBuilder.build();
|
||||
try {
|
||||
// Open input.
|
||||
afd = openAssetFileDescriptor(providerClient, wordListUri);
|
||||
// If we can't open it at all, don't even try a number of times.
|
||||
if (null == afd) return;
|
||||
originalSourceStream = afd.createInputStream();
|
||||
// Open output.
|
||||
outputFile = new File(tempFileName);
|
||||
// Just to be sure, delete the file. This may fail silently, and return false: this
|
||||
// is the right thing to do, as we just want to continue anyway.
|
||||
outputFile.delete();
|
||||
// Get the appropriate decryption method for this try
|
||||
switch (mode) {
|
||||
case COMPRESSED_CRYPTED_COMPRESSED:
|
||||
uncompressedStream =
|
||||
FileTransforms.getUncompressedStream(originalSourceStream);
|
||||
decryptedStream = FileTransforms.getDecryptedStream(uncompressedStream);
|
||||
inputStream = FileTransforms.getUncompressedStream(decryptedStream);
|
||||
break;
|
||||
case CRYPTED_COMPRESSED:
|
||||
decryptedStream = FileTransforms.getDecryptedStream(originalSourceStream);
|
||||
inputStream = FileTransforms.getUncompressedStream(decryptedStream);
|
||||
break;
|
||||
case COMPRESSED_CRYPTED:
|
||||
uncompressedStream =
|
||||
FileTransforms.getUncompressedStream(originalSourceStream);
|
||||
inputStream = FileTransforms.getDecryptedStream(uncompressedStream);
|
||||
break;
|
||||
case COMPRESSED_ONLY:
|
||||
inputStream = FileTransforms.getUncompressedStream(originalSourceStream);
|
||||
break;
|
||||
case CRYPTED_ONLY:
|
||||
inputStream = FileTransforms.getDecryptedStream(originalSourceStream);
|
||||
break;
|
||||
case NONE:
|
||||
inputStream = originalSourceStream;
|
||||
break;
|
||||
}
|
||||
bufferedInputStream = new BufferedInputStream(inputStream);
|
||||
bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(outputFile));
|
||||
checkMagicAndCopyFileTo(bufferedInputStream, bufferedOutputStream);
|
||||
bufferedOutputStream.flush();
|
||||
bufferedOutputStream.close();
|
||||
|
||||
if (SHOULD_VERIFY_CHECKSUM) {
|
||||
final String actualRawChecksum = MD5Calculator.checksum(
|
||||
new BufferedInputStream(new FileInputStream(outputFile)));
|
||||
Log.i(TAG, "Computed checksum for downloaded dictionary. Expected = "
|
||||
+ rawChecksum + " ; actual = " + actualRawChecksum);
|
||||
if (!TextUtils.isEmpty(rawChecksum) && !rawChecksum.equals(actualRawChecksum)) {
|
||||
throw new IOException(
|
||||
"Could not decode the file correctly : checksum differs");
|
||||
}
|
||||
}
|
||||
|
||||
// move the output file to the final staging file.
|
||||
final File finalFile = new File(finalFileName);
|
||||
if (!FileUtils.renameTo(outputFile, finalFile)) {
|
||||
Log.e(TAG, String.format("Failed to rename from %s to %s.",
|
||||
outputFile.getAbsoluteFile(), finalFile.getAbsoluteFile()));
|
||||
}
|
||||
|
||||
wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT,
|
||||
QUERY_PARAMETER_SUCCESS);
|
||||
if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) {
|
||||
Log.e(TAG, "Could not have the dictionary pack delete a word list");
|
||||
}
|
||||
Log.d(TAG, "Successfully copied file for wordlist ID " + wordlistId);
|
||||
// Success! Close files (through the finally{} clause) and return.
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
if (DEBUG) {
|
||||
Log.e(TAG, "Can't open word list in mode " + mode, e);
|
||||
}
|
||||
if (null != outputFile) {
|
||||
// This may or may not fail. The file may not have been created if the
|
||||
// exception was thrown before it could be. Hence, both failure and
|
||||
// success are expected outcomes, so we don't check the return value.
|
||||
outputFile.delete();
|
||||
}
|
||||
// Try the next method.
|
||||
} finally {
|
||||
// Ignore exceptions while closing files.
|
||||
closeAssetFileDescriptorAndReportAnyException(afd);
|
||||
closeCloseableAndReportAnyException(inputStream);
|
||||
closeCloseableAndReportAnyException(uncompressedStream);
|
||||
closeCloseableAndReportAnyException(decryptedStream);
|
||||
closeCloseableAndReportAnyException(bufferedInputStream);
|
||||
closeCloseableAndReportAnyException(bufferedOutputStream);
|
||||
}
|
||||
}
|
||||
|
||||
// We could not copy the file at all. This is very unexpected.
|
||||
// I'd rather not print the word list ID to the log out of security concerns
|
||||
Log.e(TAG, "Could not copy a word list. Will not be able to use it.");
|
||||
// If we can't copy it we should warn the dictionary provider so that it can mark it
|
||||
// as invalid.
|
||||
reportBrokenFileToDictionaryProvider(providerClient, clientId, wordlistId);
|
||||
}
|
||||
|
||||
public static boolean reportBrokenFileToDictionaryProvider(
|
||||
final ContentProviderClient providerClient, final String clientId,
|
||||
final String wordlistId) {
|
||||
|
@ -405,53 +258,6 @@ public final class BinaryDictionaryFileDumper {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries a content provider for word list data for some locale and stage the returned files
|
||||
*
|
||||
* This will query a content provider for word list data for a given locale, and copy the
|
||||
* files locally so that they can be mmap'ed. This may overwrite previously cached word lists
|
||||
* with newer versions if a newer version is made available by the content provider.
|
||||
* @throw FileNotFoundException if the provider returns non-existent data.
|
||||
* @throw IOException if the provider-returned data could not be read.
|
||||
*/
|
||||
public static void installDictToStagingFromContentProvider(final Locale locale,
|
||||
final Context context, final boolean hasDefaultWordList) {
|
||||
final ContentProviderClient providerClient;
|
||||
try {
|
||||
providerClient = context.getContentResolver().
|
||||
acquireContentProviderClient(getProviderUriBuilder("").build());
|
||||
} catch (final SecurityException e) {
|
||||
Log.e(TAG, "No permission to communicate with the dictionary provider", e);
|
||||
return;
|
||||
}
|
||||
if (null == providerClient) {
|
||||
Log.e(TAG, "Can't establish communication with the dictionary provider");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final List<WordListInfo> idList = getWordListWordListInfos(locale, context,
|
||||
hasDefaultWordList);
|
||||
for (WordListInfo id : idList) {
|
||||
installWordListToStaging(id.mId, id.mLocale, id.mRawChecksum, providerClient,
|
||||
context);
|
||||
}
|
||||
} finally {
|
||||
providerClient.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the dictionary if it was never requested/used.
|
||||
*
|
||||
* @param locale locale to download
|
||||
* @param context the context for resources and providers.
|
||||
* @param hasDefaultWordList whether the default wordlist exists in the resources.
|
||||
*/
|
||||
public static void downloadDictIfNeverRequested(final Locale locale,
|
||||
final Context context, final boolean hasDefaultWordList) {
|
||||
getWordListWordListInfos(locale, context, hasDefaultWordList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the data in an input stream to a target file if the magic number matches.
|
||||
*
|
||||
|
@ -533,7 +339,6 @@ public final class BinaryDictionaryFileDumper {
|
|||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = context.getResources().openRawResource(metadataResourceId);
|
||||
UpdateHandler.handleMetadata(context, inputStream, clientId);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Failed to read metadata.json from resources", e);
|
||||
} finally {
|
||||
|
|
|
@ -241,19 +241,6 @@ final public class BinaryDictionaryGetter {
|
|||
*/
|
||||
public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale,
|
||||
final Context context, boolean notifyDictionaryPackForUpdates) {
|
||||
if (notifyDictionaryPackForUpdates) {
|
||||
final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable(
|
||||
context, locale);
|
||||
// It makes sure that the first time keyboard comes up and the dictionaries are reset,
|
||||
// the DB is populated with the appropriate values for each locale. Helps in downloading
|
||||
// the dictionaries when the user enables and switches new languages before the
|
||||
// DictionaryService runs.
|
||||
BinaryDictionaryFileDumper.downloadDictIfNeverRequested(
|
||||
locale, context, hasDefaultWordList);
|
||||
|
||||
// Move a staging files to the cache ddirectories if any.
|
||||
DictionaryInfoUtils.moveStagingFilesIfExists(context);
|
||||
}
|
||||
final File[] cachedWordLists = getCachedWordLists(locale.toString(), context);
|
||||
final String mainDictId = DictionaryInfoUtils.getMainDictId(locale);
|
||||
final DictPackSettings dictPackSettings = new DictPackSettings(context);
|
||||
|
|
|
@ -32,7 +32,6 @@ import android.view.inputmethod.InputMethodManager;
|
|||
import android.view.inputmethod.InputMethodSubtype;
|
||||
|
||||
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants;
|
||||
import org.dslul.openboard.inputmethod.dictionarypack.DownloadManagerWrapper;
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet;
|
||||
import org.dslul.openboard.inputmethod.latin.settings.Settings;
|
||||
import org.dslul.openboard.inputmethod.latin.setup.SetupActivity;
|
||||
|
@ -77,12 +76,6 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver {
|
|||
final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes();
|
||||
richImm.setAdditionalInputMethodSubtypes(additionalSubtypes);
|
||||
toggleAppIcon(context);
|
||||
|
||||
// Remove all the previously scheduled downloads. This will also makes sure
|
||||
// that any erroneously stuck downloads will get cleared. (b/21797386)
|
||||
removeOldDownloads(context);
|
||||
// b/21797386
|
||||
// downloadLatestDictionaries(context);
|
||||
} else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) {
|
||||
Log.i(TAG, "Boot has been completed");
|
||||
toggleAppIcon(context);
|
||||
|
@ -110,38 +103,6 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver {
|
|||
}
|
||||
}
|
||||
|
||||
private void removeOldDownloads(Context context) {
|
||||
try {
|
||||
Log.i(TAG, "Removing the old downloads in progress of the previous keyboard version.");
|
||||
final DownloadManagerWrapper downloadManagerWrapper = new DownloadManagerWrapper(
|
||||
context);
|
||||
final DownloadManager.Query q = new DownloadManager.Query();
|
||||
// Query all the download statuses except the succeeded ones.
|
||||
q.setFilterByStatus(DownloadManager.STATUS_FAILED
|
||||
| DownloadManager.STATUS_PAUSED
|
||||
| DownloadManager.STATUS_PENDING
|
||||
| DownloadManager.STATUS_RUNNING);
|
||||
final Cursor c = downloadManagerWrapper.query(q);
|
||||
if (c != null) {
|
||||
for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) {
|
||||
final long downloadId = c
|
||||
.getLong(c.getColumnIndex(DownloadManager.COLUMN_ID));
|
||||
downloadManagerWrapper.remove(downloadId);
|
||||
Log.i(TAG, "Removed the download with Id: " + downloadId);
|
||||
}
|
||||
c.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Exception while removing old downloads.");
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadLatestDictionaries(Context context) {
|
||||
final Intent updateIntent = new Intent(
|
||||
DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION);
|
||||
context.sendBroadcast(updateIntent);
|
||||
}
|
||||
|
||||
public static void toggleAppIcon(final Context context) {
|
||||
final int appInfoFlags = context.getApplicationInfo().flags;
|
||||
final boolean isSystemApp = (appInfoFlags & ApplicationInfo.FLAG_SYSTEM) > 0;
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* 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,25 +58,7 @@ public class UserHistoryDictionary extends ExpandableBinaryDictionary {
|
|||
@UsedForTesting
|
||||
static String getUserHistoryDictName(final String name, final Locale locale,
|
||||
@Nullable final File dictFile, @Nullable final String account) {
|
||||
if (!ProductionFlags.ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY) {
|
||||
return getDictName(name, locale, dictFile);
|
||||
}
|
||||
return getUserHistoryDictNamePerAccount(name, locale, dictFile, account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the currently signed in account to determine the dictionary name.
|
||||
*/
|
||||
private static String getUserHistoryDictNamePerAccount(final String name, final Locale locale,
|
||||
@Nullable final File dictFile, @Nullable final String account) {
|
||||
if (dictFile != null) {
|
||||
return dictFile.getName();
|
||||
}
|
||||
String dictName = name + "." + locale.toString();
|
||||
if (account != null) {
|
||||
dictName += "." + account;
|
||||
}
|
||||
return dictName;
|
||||
return getDictName(name, locale, dictFile);
|
||||
}
|
||||
|
||||
// Note: This method is called by {@link DictionaryFacilitator} using Java reflection.
|
||||
|
|
|
@ -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 {
|
||||
private static final String TAG = Settings.class.getSimpleName();
|
||||
// Settings screens
|
||||
public static final String SCREEN_ACCOUNTS = "screen_accounts";
|
||||
public static final String SCREEN_THEME = "screen_theme";
|
||||
public static final String SCREEN_DEBUG = "screen_debug";
|
||||
public static final String SCREEN_GESTURE = "screen_gesture";
|
||||
|
|
|
@ -52,10 +52,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment {
|
|||
final PreferenceScreen preferenceScreen = getPreferenceScreen();
|
||||
preferenceScreen.setTitle(
|
||||
ApplicationUtils.getActivityTitleResId(getActivity(), SettingsActivity.class));
|
||||
if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) {
|
||||
final Preference accountsPreference = findPreference(Settings.SCREEN_ACCOUNTS);
|
||||
preferenceScreen.removePreference(accountsPreference);
|
||||
}
|
||||
if (!JniUtils.sHaveGestureLib) {
|
||||
final Preference gesturePreference = findPreference(Settings.SCREEN_GESTURE);
|
||||
preferenceScreen.removePreference(gesturePreference);
|
||||
|
|
|
@ -25,7 +25,6 @@ import android.util.Log;
|
|||
import android.view.inputmethod.InputMethodSubtype;
|
||||
|
||||
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
|
||||
import org.dslul.openboard.inputmethod.dictionarypack.UpdateHandler;
|
||||
import org.dslul.openboard.inputmethod.latin.AssetFileAddress;
|
||||
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter;
|
||||
import org.dslul.openboard.inputmethod.latin.R;
|
||||
|
@ -61,8 +60,6 @@ public class DictionaryInfoUtils {
|
|||
// 6 digits - unicode is limited to 21 bits
|
||||
private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6;
|
||||
|
||||
private static final String TEMP_DICT_FILE_SUB = UpdateHandler.TEMP_DICT_FILE_SUB;
|
||||
|
||||
public static class DictionaryInfo {
|
||||
private static final String LOCALE_COLUMN = "locale";
|
||||
private static final String WORDLISTID_COLUMN = "id";
|
||||
|
@ -207,17 +204,6 @@ public class DictionaryInfoUtils {
|
|||
return new File(DictionaryInfoUtils.getWordListStagingDirectory(context)).listFiles();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static File[] getUnusedDictionaryList(final Context context) {
|
||||
return context.getFilesDir().listFiles(new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(File dir, String filename) {
|
||||
return !TextUtils.isEmpty(filename) && filename.endsWith(".dict")
|
||||
&& filename.contains(TEMP_DICT_FILE_SUB);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the category for a given file name.
|
||||
*
|
||||
|
@ -253,75 +239,6 @@ public class DictionaryInfoUtils {
|
|||
return absoluteDirectoryName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a file name for the id and locale passed as an argument.
|
||||
*
|
||||
* In the current implementation the file name returned will always be unique for
|
||||
* any id/locale pair, but please do not expect that the id can be the same for
|
||||
* different dictionaries with different locales. An id should be unique for any
|
||||
* dictionary.
|
||||
* The file name is pretty much an URL-encoded version of the id inside a directory
|
||||
* named like the locale, except it will also escape characters that look dangerous
|
||||
* to some file systems.
|
||||
* @param id the id of the dictionary for which to get a file name
|
||||
* @param locale the locale for which to get the file name as a string
|
||||
* @param context the context to use for getting the directory
|
||||
* @return the name of the file to be created
|
||||
*/
|
||||
public static String getCacheFileName(String id, String locale, Context context) {
|
||||
final String fileName = replaceFileNameDangerousCharacters(id);
|
||||
return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
|
||||
}
|
||||
|
||||
public static String getStagingFileName(String id, String locale, Context context) {
|
||||
final String stagingDirectory = getWordListStagingDirectory(context);
|
||||
// create the directory if it does not exist.
|
||||
final File directory = new File(stagingDirectory);
|
||||
if (!directory.exists()) {
|
||||
if (!directory.mkdirs()) {
|
||||
Log.e(TAG, "Could not create the staging directory.");
|
||||
}
|
||||
}
|
||||
// e.g. id="main:en_in", locale ="en_IN"
|
||||
final String fileName = replaceFileNameDangerousCharacters(
|
||||
locale + TEMP_DICT_FILE_SUB + id);
|
||||
return stagingDirectory + File.separator + fileName;
|
||||
}
|
||||
|
||||
public static void moveStagingFilesIfExists(Context context) {
|
||||
final File[] stagingFiles = DictionaryInfoUtils.getStagingDirectoryList(context);
|
||||
if (stagingFiles != null && stagingFiles.length > 0) {
|
||||
for (final File stagingFile : stagingFiles) {
|
||||
final String fileName = stagingFile.getName();
|
||||
final int index = fileName.indexOf(TEMP_DICT_FILE_SUB);
|
||||
if (index == -1) {
|
||||
// This should never happen.
|
||||
Log.e(TAG, "Staging file does not have ___ substring.");
|
||||
continue;
|
||||
}
|
||||
final String[] localeAndFileId = fileName.split(TEMP_DICT_FILE_SUB);
|
||||
if (localeAndFileId.length != 2) {
|
||||
Log.e(TAG, String.format("malformed staging file %s. Deleting.",
|
||||
stagingFile.getAbsoluteFile()));
|
||||
stagingFile.delete();
|
||||
continue;
|
||||
}
|
||||
|
||||
final String locale = localeAndFileId[0];
|
||||
// already escaped while moving to staging.
|
||||
final String fileId = localeAndFileId[1];
|
||||
final String cacheDirectoryForLocale = getCacheDirectoryForLocale(locale, context);
|
||||
final String cacheFilename = cacheDirectoryForLocale + File.separator + fileId;
|
||||
final File cacheFile = new File(cacheFilename);
|
||||
// move the staging file to cache file.
|
||||
if (!FileUtils.renameTo(stagingFile, cacheFile)) {
|
||||
Log.e(TAG, String.format("Failed to rename from %s to %s.",
|
||||
stagingFile.getAbsoluteFile(), cacheFile.getAbsoluteFile()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isMainWordListId(final String id) {
|
||||
final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
|
||||
// An id is supposed to be in format category:locale, so splitting on the separator
|
||||
|
@ -521,25 +438,6 @@ public class DictionaryInfoUtils {
|
|||
}
|
||||
}
|
||||
|
||||
// Retrieve downloaded dictionaries from the unused dictionaries.
|
||||
File[] unusedDictionaryList = getUnusedDictionaryList(context);
|
||||
if (unusedDictionaryList != null) {
|
||||
for (File dictionaryFile : unusedDictionaryList) {
|
||||
String fileName = dictionaryFile.getName();
|
||||
int index = fileName.indexOf(TEMP_DICT_FILE_SUB);
|
||||
if (index == -1) {
|
||||
continue;
|
||||
}
|
||||
String locale = fileName.substring(0, index);
|
||||
DictionaryInfo dictionaryInfo = createDictionaryInfoForUnCachedFile(
|
||||
AssetFileAddress.makeFromFile(dictionaryFile),
|
||||
LocaleUtils.constructLocaleFromString(locale));
|
||||
if (dictionaryInfo != null) {
|
||||
addOrUpdateDictInfo(dictList, dictionaryInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve files from assets
|
||||
final Resources resources = context.getResources();
|
||||
final AssetManager assets = resources.getAssets();
|
||||
|
|
|
@ -18,7 +18,6 @@ package org.dslul.openboard.inputmethod.latin.utils;
|
|||
|
||||
import org.dslul.openboard.inputmethod.dictionarypack.DictionarySettingsFragment;
|
||||
import org.dslul.openboard.inputmethod.latin.about.AboutPreferences;
|
||||
import org.dslul.openboard.inputmethod.latin.settings.AccountsSettingsFragment;
|
||||
import org.dslul.openboard.inputmethod.latin.settings.AdvancedSettingsFragment;
|
||||
import org.dslul.openboard.inputmethod.latin.settings.AppearanceSettingsFragment;
|
||||
import org.dslul.openboard.inputmethod.latin.settings.CorrectionSettingsFragment;
|
||||
|
@ -42,7 +41,6 @@ public class FragmentUtils {
|
|||
sLatinImeFragments.add(DictionarySettingsFragment.class.getName());
|
||||
sLatinImeFragments.add(AboutPreferences.class.getName());
|
||||
sLatinImeFragments.add(PreferencesSettingsFragment.class.getName());
|
||||
sLatinImeFragments.add(AccountsSettingsFragment.class.getName());
|
||||
sLatinImeFragments.add(AppearanceSettingsFragment.class.getName());
|
||||
sLatinImeFragments.add(ThemeSettingsFragment.class.getName());
|
||||
sLatinImeFragments.add(CustomInputStyleSettingsFragment.class.getName());
|
||||
|
|
|
@ -22,10 +22,6 @@
|
|||
android:fragment="org.dslul.openboard.inputmethod.latin.settings.PreferencesSettingsFragment"
|
||||
android:title="@string/settings_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
|
||||
android:fragment="org.dslul.openboard.inputmethod.latin.settings.AppearanceSettingsFragment"
|
||||
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