/*
* Copyright (C) 2008 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 info.guardianproject.otr.app.im.app;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.ContentUris;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.net.Uri;
import android.net.Uri.Builder;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.os.RemoteException;
import android.os.StatFs;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import info.guardianproject.cacheword.CacheWordActivityHandler;
import info.guardianproject.cacheword.CacheWordService;
import info.guardianproject.cacheword.ICacheWordSubscriber;
import info.guardianproject.otr.OtrAndroidKeyManagerImpl;
import info.guardianproject.otr.app.im.IImConnection;
import info.guardianproject.otr.app.im.R;
import info.guardianproject.otr.app.im.engine.ImConnection;
import info.guardianproject.otr.app.im.provider.Imps;
import info.guardianproject.otr.app.im.provider.SQLCipherOpenHelper;
import java.io.File;
import net.hockeyapp.android.UpdateManager;
public class WelcomeActivity extends ThemeableActivity implements ICacheWordSubscriber {
private static final String TAG = "WelcomeActivity";
private Cursor mProviderCursor;
private ImApp mApp;
private SimpleAlertHandler mHandler;
private SignInHelper mSignInHelper;
private boolean mDoSignIn = true;
private ProgressDialog dialog;
static final String[] PROVIDER_PROJECTION = { Imps.Provider._ID, Imps.Provider.NAME,
Imps.Provider.FULLNAME, Imps.Provider.CATEGORY,
Imps.Provider.ACTIVE_ACCOUNT_ID,
Imps.Provider.ACTIVE_ACCOUNT_USERNAME,
Imps.Provider.ACTIVE_ACCOUNT_PW,
Imps.Provider.ACTIVE_ACCOUNT_LOCKED,
Imps.Provider.ACTIVE_ACCOUNT_KEEP_SIGNED_IN,
Imps.Provider.ACCOUNT_PRESENCE_STATUS,
Imps.Provider.ACCOUNT_CONNECTION_STATUS, };
static final int PROVIDER_ID_COLUMN = 0;
static final int PROVIDER_NAME_COLUMN = 1;
static final int PROVIDER_FULLNAME_COLUMN = 2;
static final int PROVIDER_CATEGORY_COLUMN = 3;
static final int ACTIVE_ACCOUNT_ID_COLUMN = 4;
static final int ACTIVE_ACCOUNT_USERNAME_COLUMN = 5;
static final int ACTIVE_ACCOUNT_PW_COLUMN = 6;
static final int ACTIVE_ACCOUNT_LOCKED = 7;
static final int ACTIVE_ACCOUNT_KEEP_SIGNED_IN = 8;
static final int ACCOUNT_PRESENCE_STATUS = 9;
static final int ACCOUNT_CONNECTION_STATUS = 10;
private CacheWordActivityHandler mCacheWord = null;
private boolean mDoLock;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mApp = (ImApp)getApplication();
mHandler = new MyHandler(this);
mSignInHelper = new SignInHelper(this);
Intent intent = getIntent();
mDoSignIn = intent.getBooleanExtra("doSignIn", true);
mDoLock = intent.getBooleanExtra("doLock", false);
if (!mDoLock)
{
if (checkMediaStoreFile()) {
return;
}
mApp.maybeInit(this);
}
if (ImApp.mUsingCacheword)
connectToCacheWord();
else
{
if (openEncryptedStores(null, false)) {
try
{
ChatFileStore.initWithoutPassword(this);
}
catch (Exception e)
{
Log.d(ImApp.LOG_TAG,"unable to mount VFS store"); //but let's not crash the whole app right now
}
} else {
connectToCacheWord(); //first time setup
}
}
// if we have an incoming contact, send it to the right place
String scheme = intent.getScheme();
if(TextUtils.equals(scheme, "xmpp"))
{
intent.setClass(this, AddContactActivity.class);
startActivity(intent);
finish();
return;
}
}
private void connectToCacheWord ()
{
mCacheWord = new CacheWordActivityHandler(this, (ICacheWordSubscriber)this);
mCacheWord.connectToService();
}
@SuppressWarnings("deprecation")
private boolean cursorUnlocked(String pKey, boolean allowCreate) {
try {
Uri uri = Imps.Provider.CONTENT_URI_WITH_ACCOUNT;
Builder builder = uri.buildUpon();
if (pKey != null)
builder.appendQueryParameter(ImApp.CACHEWORD_PASSWORD_KEY, pKey);
if (!allowCreate)
builder = builder.appendQueryParameter(ImApp.NO_CREATE_KEY, "1");
uri = builder.build();
mProviderCursor = managedQuery(uri,
PROVIDER_PROJECTION, Imps.Provider.CATEGORY + "=?" /* selection */,
new String[] { ImApp.IMPS_CATEGORY } /* selection args */,
Imps.Provider.DEFAULT_SORT_ORDER);
if (mProviderCursor != null)
{
ImPluginHelper.getInstance(this).loadAvailablePlugins();
mProviderCursor.moveToFirst();
return true;
}
else
{
return false;
}
} catch (Exception e) {
// Only complain if we thought this password should succeed
if (allowCreate) {
Log.e(ImApp.LOG_TAG, e.getMessage(), e);
Toast.makeText(this, getString(R.string.error_welcome_database), Toast.LENGTH_LONG).show();
finish();
}
// needs to be unlocked
return false;
}
}
// private void initCursor(String dbKey) {
//
// mProviderCursor = managedQuery(Imps.Provider.CONTENT_URI_WITH_ACCOUNT, PROVIDER_PROJECTION,
// Imps.Provider.CATEGORY + "=?" /* selection */,
// new String[] { ImApp.IMPS_CATEGORY } /* selection args */, null);
// doOnResume();
// }
@Override
protected void onPause() {
if (mHandler != null)
mHandler.unregisterForBroadcastEvents();
super.onPause();
if (mCacheWord != null)
mCacheWord.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mCacheWord != null)
mCacheWord.disconnect();
if (dialog != null)
dialog.dismiss();
}
@Override
protected void onResume() {
super.onResume();
if (mCacheWord != null)
mCacheWord.onResume();
}
private void doOnResume() {
mHandler.registerForBroadcastEvents();
int countAvailable = accountsAvailable();
if (countAvailable == 1) {
// If just one account is available for auto-signin, go there immediately after service starts trying
// to connect.
mSignInHelper.setSignInListener(new SignInHelper.SignInListener() {
@Override
public void connectedToService() {
}
@Override
public void stateChanged(int state, long accountId) {
if (state == ImConnection.LOGGING_IN) {
mSignInHelper.goToAccount(accountId);
}
}
});
} else {
mSignInHelper.setSignInListener(null);
}
Intent intent = getIntent();
if (intent != null && intent.getAction() != null && (!intent.getAction().equals(Intent.ACTION_MAIN)))
{
handleIntentAPILaunch(intent);
}
else
{
if (countAvailable > 0 && mDoSignIn && mProviderCursor.moveToFirst()) {
do {
if (!mProviderCursor.isNull(ACTIVE_ACCOUNT_ID_COLUMN)) {
int state = mProviderCursor.getInt(ACCOUNT_CONNECTION_STATUS);
long accountId = mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN);
if (mProviderCursor.getInt(ACTIVE_ACCOUNT_KEEP_SIGNED_IN) != 0) {
signIn(accountId);
}
}
} while (mProviderCursor.moveToNext());
}
startActivity(new Intent(getBaseContext(), NewChatActivity.class));
finish();
}
}
private void signIn(long accountId) {
if (accountId == 0) {
Log.w(TAG, "signIn: account id is 0, bail");
return;
}
boolean isAccountEditable = mProviderCursor.getInt(ACTIVE_ACCOUNT_LOCKED) == 0;
if (isAccountEditable && mProviderCursor.isNull(ACTIVE_ACCOUNT_PW_COLUMN)) {
// no password, edit the account
if (Log.isLoggable(TAG, Log.DEBUG))
Log.i(TAG, "no pw for account " + accountId);
Intent intent = getEditAccountIntent();
startActivity(intent);
finish();
return;
}
long providerId = mProviderCursor.getLong(PROVIDER_ID_COLUMN);
String password = mProviderCursor.getString(ACTIVE_ACCOUNT_PW_COLUMN);
boolean isActive = false; // TODO(miron)
mSignInHelper.signIn(password, providerId, accountId, isActive);
}
private boolean isSignedIn(Cursor cursor) {
int connectionStatus = cursor.getInt(ACCOUNT_CONNECTION_STATUS);
return connectionStatus == Imps.ConnectionStatus.ONLINE;
}
private int accountsAvailable() {
if (!mProviderCursor.moveToFirst()) {
return 0;
}
int count = 0;
do {
if (!mProviderCursor.isNull(ACTIVE_ACCOUNT_PW_COLUMN) &&
!mProviderCursor.isNull(ACTIVE_ACCOUNT_ID_COLUMN) &&
mProviderCursor.getInt(ACTIVE_ACCOUNT_KEEP_SIGNED_IN) != 0) {
count++;
}
} while (mProviderCursor.moveToNext());
return count;
}
void handleIntentAPILaunch (Intent srcIntent)
{
Intent intent = new Intent(this, ImUrlActivity.class);
intent.setAction(srcIntent.getAction());
if (srcIntent.getData() != null)
intent.setData(srcIntent.getData());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (srcIntent.getExtras()!= null)
intent.putExtras(srcIntent.getExtras());
startActivity(intent);
setIntent(null);
finish();
}
Intent getEditAccountIntent() {
Intent intent = new Intent(Intent.ACTION_EDIT, ContentUris.withAppendedId(
Imps.Account.CONTENT_URI, mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN)));
intent.putExtra("isSignedIn", isSignedIn(mProviderCursor));
intent.addCategory(getProviderCategory(mProviderCursor));
return intent;
}
private String getProviderCategory(Cursor cursor) {
return cursor.getString(PROVIDER_CATEGORY_COLUMN);
}
private final static class MyHandler extends SimpleAlertHandler {
public MyHandler(Activity activity) {
super(activity);
}
@Override
public void handleMessage(Message msg) {
if (msg.what == ImApp.EVENT_CONNECTION_DISCONNECTED) {
promptDisconnectedEvent(msg);
}
super.handleMessage(msg);
}
}
@Override
public void onCacheWordUninitialized() {
Log.d(ImApp.LOG_TAG,"cache word uninit");
if (mDoLock) {
completeShutdown();
} else {
showLockScreen();
}
finish();
}
void showLockScreen() {
Intent intent = new Intent(this, LockScreenActivity.class);
Intent returnIntent = getIntent();
returnIntent.putExtra("doSignIn", mDoSignIn);
intent.putExtra("originalIntent", returnIntent);
startActivity(intent);
}
@Override
public void onCacheWordLocked() {
if (mDoLock) {
Log.d(ImApp.LOG_TAG, "cacheword lock requested but already locked");
} else {
showLockScreen();
}
finish();
}
@Override
public void onCacheWordOpened() {
if (mDoLock) {
completeShutdown();
return;
}
byte[] encryptionKey = mCacheWord.getEncryptionKey();
openEncryptedStores(encryptionKey, true);
ChatFileStore.init(this, mCacheWord.getEncryptionKey());
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static void shutdownAndLock(Activity activity) {
ImApp app = (ImApp) activity.getApplication();
if (app != null) {
for (IImConnection conn : app.getActiveConnections()) {
try {
conn.logout();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
Intent intent = new Intent(activity, WelcomeActivity.class);
// Request lock
intent.putExtra("doLock", true);
// Clear the backstack
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= 11)
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
activity.startActivity(intent);
activity.finish();
}
private void completeShutdown ()
{
/* ignore unmount errors and quit ASAP. Threads actively using the VFS will
* cause IOCipher's VirtualFileSystem.unmount() to throw an IllegalStateException */
try {
ChatFileStore.unmount();
} catch (IllegalStateException e) {
e.printStackTrace();
}
new AsyncTask<String, Void, String>() {
@Override
protected void onPreExecute() {
if (mApp.getActiveConnections().size() > 0)
{
dialog = new ProgressDialog(WelcomeActivity.this);
dialog.setCancelable(true);
dialog.setMessage(getString(R.string.signing_out_wait));
dialog.show();
}
}
@Override
protected String doInBackground(String... params) {
boolean stillConnected = true;
while (stillConnected)
{
try{
IImConnection conn = mApp.getActiveConnections().iterator().next();
if (conn.getState() == ImConnection.DISCONNECTED || conn.getState() == ImConnection.LOGGING_OUT)
{
stillConnected = false;
}
else
{
conn.logout();
stillConnected = true;
}
Thread.sleep(500);
}catch(Exception e){}
}
return "";
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
if (dialog != null)
dialog.dismiss();
mApp.forceStopImService();
Imps.clearPassphrase(mApp);
if (mCacheWord != null)
{
mCacheWord.manuallyLock();
}
Intent cacheWordIntent = CacheWordService
.getBlankServiceIntent(getApplicationContext());
stopService(cacheWordIntent);
finish();
}
}.execute();
}
private boolean checkMediaStoreFile() {
/* First set location based on pref, then override based on where the file is.
* This crazy logic is necessary to support old installs that used logic that
* is not really predictable, since it was based on whether the SD card was
* present or not. */
File internalDbFile = new File(ChatFileStore.getInternalDbFilePath(this));
boolean internalDbFileUsabe = internalDbFile.isFile() && internalDbFile.canWrite();
boolean externalDbFileUsable = false;
java.io.File externalFilesDir = getExternalFilesDir(null);
if (externalFilesDir != null) {
File externalDbFile = new File(ChatFileStore.getExternalDbFilePath(this));
externalDbFileUsable = externalDbFile.isFile() && externalDbFile.canWrite();
}
final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
boolean isPrefSet = settings.contains(
getString(R.string.key_store_media_on_external_storage_pref));
boolean storeMediaOnExternalStorage;
if (isPrefSet) {
storeMediaOnExternalStorage = settings.getBoolean(
getString(R.string.key_store_media_on_external_storage_pref), false);
if (storeMediaOnExternalStorage && !externalDbFileUsable) {
Intent i = new Intent(this, MissingChatFileStoreActivity.class);
startActivity(i);
finish();
return true;
}
} else {
/* only use external if file already exists only there or internal is almost full */
boolean forceExternalStorage = !enoughSpaceInInternalStorage(internalDbFile);
if (!internalDbFileUsabe && (externalDbFileUsable || forceExternalStorage)) {
storeMediaOnExternalStorage = true;
} else {
storeMediaOnExternalStorage = false;
}
Editor editor = settings.edit();
editor.putBoolean(getString(R.string.key_store_media_on_external_storage_pref),
storeMediaOnExternalStorage);
editor.apply();
}
return false;
}
private static boolean enoughSpaceInInternalStorage(File f) {
StatFs stat = new StatFs(f.getParent());
long freeSizeInBytes = stat.getAvailableBlocks() * (long) stat.getBlockSize();
return freeSizeInBytes > 536870912; // 512 MB
}
private boolean openEncryptedStores(byte[] key, boolean allowCreate) {
String pkey = (key != null) ? new String(SQLCipherOpenHelper.encodeRawKey(key)) : "";
OtrAndroidKeyManagerImpl.setKeyStorePassword(pkey);
if (cursorUnlocked(pkey, allowCreate)) {
if (mDoLock)
completeShutdown();
else
doOnResume();
return true;
} else {
return false;
}
}
private void checkForUpdates() {
// Remove this for store builds!
UpdateManager.register(this, ImApp.HOCKEY_APP_ID);
}
}