/*
* Copyright 2012 The Stanford MobiSocial Laboratory
*
* 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 mobisocial.musubi.ui.widget;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import mobisocial.crypto.IBHashedIdentity;
import mobisocial.musubi.App;
import mobisocial.musubi.R;
import mobisocial.musubi.model.MIdentity;
import mobisocial.musubi.model.helpers.DatabaseManager;
import mobisocial.musubi.ui.util.UiUtil;
import android.app.ActionBar.LayoutParams;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Color;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.CursorAdapter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.MultiAutoCompleteTextView;
import android.widget.TextView;
/**
* Allows the user to type in multiple identities. Get the identities with
* getSelectedIdentities();
*/
public class MultiIdentitySelector extends MultiAutoCompleteTextView {
private final ContactAdapter mContactAdapter;
private DatabaseManager mDatabaseManger;
private OnIdentitiesUpdatedListener mIdentitiesUpdatedListener;
private OnRequestAddIdentityListener mRequestAddIdentityListener;
boolean ranOnce = false;
private String mPreviousText;
private int mPreviousCaret;
private CommaTokenizer mTokenizer;
private boolean mIdentityMatchesDirty = false;
private LinkedHashSet<MIdentity> mIdentityMatches;
public static final String TAG = "MultiIdentitySelector";
public MultiIdentitySelector(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
mContactAdapter = new ContactAdapter(context);
init(context);
}
public MultiIdentitySelector(Context context) {
super(context);
mContactAdapter = new ContactAdapter(context);
init(context);
}
void init(Context context) {
mPreviousText = "";
mPreviousCaret = 0;
mTokenizer = new MultiAutoCompleteTextView.CommaTokenizer();
setTokenizer(mTokenizer);
setAdapter(mContactAdapter);
setOnItemClickListener(mContactAdapter);
setDropDownWidth(LayoutParams.FILL_PARENT);
setHint("Type some names...");
// Set threshold to 0 after first content load to avoid insta-dropdown.
setThreshold(0);
setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME);
SQLiteOpenHelper databaseSource = App.getDatabaseSource(context);
mDatabaseManger = new DatabaseManager(databaseSource);
mIdentityMatches = new LinkedHashSet<MIdentity>();
addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
mPreviousCaret = getSelectionStart();
mPreviousText = s.toString();
}
@Override
public void afterTextChanged(Editable s) {
handlePotentialIdentityUpdate();
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
boolean result = super.onTouchEvent(event);
if(getText().length() == 0 && action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
showDropDown();
}
return result;
}
public interface OnIdentitiesUpdatedListener {
void onIdentitiesUpdated();
}
public interface OnRequestAddIdentityListener {
void onRequestAddIdentity(String enteredText);
}
class ContactAdapter extends CursorAdapter implements Filterable, OnItemClickListener {
private final String[] sMusubiProjection = new String[] { MIdentity.COL_ID };
private static final String sSortOrder = MIdentity.COL_CLAIMED + " desc, " +
MIdentity.COL_NAME + " desc";
private final SQLiteOpenHelper mmDatabase;
private final Map<String, Long> mmEntryMap = new HashMap<String, Long>();
private static final int c_id = 0;
private ContactAdapter(Context context) {
super(context, null);
mmDatabase = App.getDatabaseSource(context);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
TextView name = (TextView) view.findViewById(R.id.name_text);
TextView principal = (TextView) view.findViewById(R.id.principal_text);
ImageView icon = (ImageView) view.findViewById(R.id.icon);
long identityId = cursor.getLong(c_id);
view.setTag(identityId);
MIdentity ident = mDatabaseManger.getIdentitiesManager().getIdentityForId(identityId);
if (cursor.getPosition() == cursor.getCount() - 1) {
name.setText("Add a new contact");
icon.setImageResource(R.drawable.ic_add_contact_holo_light);
principal.setVisibility(View.GONE);
} else {
if (!ident.claimed_) { // TODO: find white/gray split
view.setBackgroundColor(Color.LTGRAY);
} else {
view.setBackgroundColor(Color.TRANSPARENT);
}
name.setText(UiUtil.safeNameForIdentity(ident));
principal.setText(UiUtil.safePrincipalForIdentity(ident));
principal.setVisibility(View.VISIBLE);
icon.setImageBitmap(UiUtil.safeGetContactThumbnail(context, mDatabaseManger.getIdentitiesManager(), ident));
}
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
final LayoutInflater inflater = LayoutInflater.from(context);
View v = inflater.inflate(R.layout.contact_autocomplete_item, parent, false);
bindView(v, context, cursor);
return v;
}
@Override
public CharSequence convertToString(Cursor cursor) {
if (cursor == null) {
return "";
}
long identityId = cursor.getLong(c_id);
MIdentity ident = mDatabaseManger.getIdentitiesManager().getIdentityForId(identityId);
if(ident == null)
return "";
return UiUtil.safeNameForIdentity(ident);
}
@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
assert(getFilterQueryProvider() == null);
String musubiSelect = null;
String[] args = null;
String localType = Integer.toString(IBHashedIdentity.Authority.Local.ordinal());
if (constraint == null || constraint.length() == 0) {
return new AddNewContactCursor();
} else {
StringBuilder select = new StringBuilder(100)
.append("((UPPER(").append(MIdentity.COL_NAME).append(") LIKE ? OR UPPER(")
.append(MIdentity.COL_MUSUBI_NAME).append(") LIKE ? OR UPPER(")
.append(MIdentity.COL_PRINCIPAL).append(") LIKE ?))")
.append(" AND ").append(MIdentity.COL_TYPE).append("!=").append(localType);
String param = constraint.toString().toUpperCase();
String arg1 = "%" + param + "%";
args = new String[] { arg1, arg1, arg1 };
if (constraint.length() < 2) {
select.append(" AND ").append(MIdentity.COL_CLAIMED).append("=1");
}
musubiSelect = select.toString();
}
Cursor identitiesCursor = mmDatabase.getWritableDatabase().query(MIdentity.TABLE, sMusubiProjection,
musubiSelect, args, null, null, sSortOrder);
AddNewContactCursor addNewContactCursor = new AddNewContactCursor();
return new MergeCursor(new Cursor[]{ identitiesCursor, addNewContactCursor });
}
@Override
public void onItemClick(AdapterView<?> adapter, View v, int pos, long id) {
String key = ((TextView)v.findViewById(R.id.name_text)).getText().toString();
Long val = (Long)v.getTag();
if (pos == adapter.getCount() - 1) {
int start = mTokenizer.findTokenStart(mPreviousText, mPreviousCaret);
int end = mTokenizer.findTokenEnd(mPreviousText, mPreviousCaret);
String entered = null;
if(end - start > 0) {
entered = mPreviousText.substring(start, end);
}
String dirtyString = getText().toString();
String cleanString = dirtyString.replaceAll(", ,", ", ");
if (cleanString.equalsIgnoreCase(", ")) {
setText("");
}
else {
setText(cleanString);
setSelection(getText().length()-1);
}
if (mRequestAddIdentityListener != null) {
mRequestAddIdentityListener.onRequestAddIdentity(entered);
}
//clearSelectedIdentities();
} else {
//trim or the deletion code will always assume the person is removed.
mmEntryMap.put(key.trim(), val);
}
handlePotentialIdentityUpdate();
}
}
/**
* Removes entered identities.
*/
public void clearSelectedIdentities() {
mContactAdapter.mmEntryMap.clear();
setText("");
}
synchronized void handlePotentialIdentityUpdate() {
mIdentityMatchesDirty = true;
LinkedHashSet<MIdentity> matches = getSelectedIdentities();
mIdentityMatchesDirty = false;
if (matches.size() == mIdentityMatches.size()
&& matches.containsAll(mIdentityMatches)) {
return;
}
mIdentityMatches = matches;
if (mIdentitiesUpdatedListener != null) {
mIdentitiesUpdatedListener.onIdentitiesUpdated();
}
}
public void setOnIdentitiesUpdatedListener(OnIdentitiesUpdatedListener listener) {
mIdentitiesUpdatedListener = listener;
}
public LinkedHashSet<MIdentity> getSelectedIdentities() {
if (!mIdentityMatchesDirty) {
return mIdentityMatches;
}
LinkedHashSet<MIdentity> hits = new LinkedHashSet<MIdentity>();
String[] names = getText().toString().split(",");
for (String name : names) {
name = name.trim();
if (name.length() == 0) {
continue;
}
if (mContactAdapter.mmEntryMap.containsKey(name)) {
long cid = mContactAdapter.mmEntryMap.get(name);
hits.add(mDatabaseManger.getIdentitiesManager().getIdentityForId(cid));
}
}
return hits;
}
/**
* A stub cursor to be used in the autocomplete adapter allowing the user
* to add a new contact to the address book.
*/
class AddNewContactCursor extends MatrixCursor {
public AddNewContactCursor() {
super(new String[] { "_id" });
addRow(new Object[] { 0 });
}
}
public void setOnRequestAddIdentityListener(
OnRequestAddIdentityListener listener) {
mRequestAddIdentityListener = listener;
}
public void addIdentity(String name, long identId) {
mContactAdapter.mmEntryMap.put(name.trim(), identId);
if(getText().toString().trim().length() == 0) {
setText(name + ", ");
} else {
setText(getText().toString().trim() + ", " + name + ", ");
}
setSelection(getText().length());
}
}