rebranding: rename sflphone to ring
Refs #69972
Refs #70084
Change-Id: I152468471b02ff3b118c083cd285786e9e286fcc
diff --git a/ring-android/src/cx/ring/adapters/AccountSelectionAdapter.java b/ring-android/src/cx/ring/adapters/AccountSelectionAdapter.java
new file mode 100644
index 0000000..9b480bf
--- /dev/null
+++ b/ring-android/src/cx/ring/adapters/AccountSelectionAdapter.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.adapters;
+
+import java.io.File;
+import java.util.ArrayList;
+
+import cx.ring.R;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+import cx.ring.model.account.Account;
+
+public class AccountSelectionAdapter extends BaseAdapter {
+
+ private static final String TAG = AccountSelectionAdapter.class.getSimpleName();
+
+ ArrayList<Account> accounts;
+ Context mContext;
+ int selectedAccount = -1;
+ static final String DEFAULT_ACCOUNT_ID = "IP2IP";
+
+ public AccountSelectionAdapter(Context cont, ArrayList<Account> newList) {
+ super();
+ accounts = newList;
+ mContext = cont;
+ }
+
+ @Override
+ public int getCount() {
+ return accounts.size();
+ }
+
+ @Override
+ public Account getItem(int pos) {
+ return accounts.get(pos);
+ }
+
+ @Override
+ public long getItemId(int pos) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int pos, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ AccountView entryView = null;
+
+ if (rowView == null) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ rowView = inflater.inflate(R.layout.item_account, null);
+
+ entryView = new AccountView();
+ entryView.alias = (TextView) rowView.findViewById(R.id.account_alias);
+ entryView.host = (TextView) rowView.findViewById(R.id.account_host);
+ entryView.select = (ImageView) rowView.findViewById(R.id.account_selected);
+ rowView.setTag(entryView);
+ } else {
+ entryView = (AccountView) rowView.getTag();
+ }
+
+ entryView.alias.setText(accounts.get(pos).getAlias());
+
+ entryView.host.setText(accounts.get(pos).getHost() + " - " + accounts.get(pos).getRegistered_state());
+ // accManager.displayAccountDetails(accounts.get(pos), entryView);
+ if (pos == selectedAccount) {
+ entryView.select.setVisibility(View.VISIBLE);
+ } else {
+ entryView.select.setVisibility(View.GONE);
+ }
+
+ return rowView;
+ }
+
+ public Account getAccount(String accountID) {
+ for(Account acc : accounts) {
+ if(acc.getAccountID().contentEquals(accountID))
+ return acc;
+ }
+ return null;
+ }
+
+ /*********************
+ * ViewHolder Pattern
+ *********************/
+ public class AccountView {
+ public TextView alias;
+ public TextView host;
+ public ImageView select;
+ }
+
+ public void setSelectedAccount(int pos) {
+ selectedAccount = pos;
+ }
+
+ public Account getSelectedAccount() {
+ if (selectedAccount == -1) {
+ return null;
+ }
+ return accounts.get(selectedAccount);
+ }
+
+ public void removeAll() {
+ accounts.clear();
+ notifyDataSetChanged();
+
+ }
+
+ public void addAll(ArrayList<Account> results) {
+ accounts.addAll(results);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Modify state of specific account
+ */
+ public void updateAccount(String accoundID, String state, int code) {
+ Log.i(TAG, "updateAccount");
+
+ for (Account a : accounts) {
+ if (a.getAccountID().contentEquals(accoundID)) {
+ a.setRegistered_state(state);
+ notifyDataSetChanged();
+ return;
+ }
+ }
+ }
+
+ public String getAccountOrder() {
+ String result = DEFAULT_ACCOUNT_ID + File.separator;
+ String selectedID = accounts.get(selectedAccount).getAccountID();
+ result += selectedID + File.separator;
+
+ for (Account a : accounts) {
+ if (a.getAccountID().contentEquals(selectedID)) {
+ continue;
+ }
+ result += a.getAccountID() + File.separator;
+ }
+
+ return result;
+ }
+
+}
diff --git a/ring-android/src/cx/ring/adapters/ContactPictureTask.java b/ring-android/src/cx/ring/adapters/ContactPictureTask.java
new file mode 100644
index 0000000..5596a06
--- /dev/null
+++ b/ring-android/src/cx/ring/adapters/ContactPictureTask.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.adapters;
+
+import java.io.InputStream;
+
+import cx.ring.R;
+import cx.ring.model.CallContact;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.widget.ImageView;
+
+public class ContactPictureTask implements Runnable {
+ private ImageView view;
+ private CallContact contact;
+ private ContentResolver cr;
+ private static int PADDING = 5;
+
+ // private final String TAG = ContactPictureTask.class.getSimpleName();
+
+ public ContactPictureTask(Context context, ImageView element, CallContact item) {
+ contact = item;
+ cr = context.getContentResolver();
+ view = element;
+ }
+
+ public static Bitmap loadContactPhoto(ContentResolver cr, long id) {
+ if(id == -1)
+ return null;
+ Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
+ InputStream input = ContactsContract.Contacts.openContactPhotoInputStream(cr, uri, true);
+ if (input == null) {
+ return null;
+ }
+ return BitmapFactory.decodeStream(input);
+ }
+
+ @Override
+ public void run() {
+ Bitmap photo_bmp;
+ try {
+ photo_bmp = loadContactPhoto(cr, contact.getId());
+ } catch (IllegalArgumentException e) {
+ photo_bmp = null;
+ }
+
+ int dpiPadding = (int) (PADDING * view.getResources().getDisplayMetrics().density);
+
+ if (photo_bmp == null) {
+ photo_bmp = decodeSampledBitmapFromResource(view.getResources(), R.drawable.ic_contact_picture, view.getWidth(), view.getHeight());
+ }
+
+ int w = photo_bmp.getWidth(), h = photo_bmp.getHeight();
+ if (w > h) {
+ w = h;
+ } else if (h > w) {
+ h = w;
+ }
+
+ final Bitmap externalBMP = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+
+ BitmapShader shader;
+ shader = new BitmapShader(photo_bmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setShader(shader);
+ Canvas internalCanvas = new Canvas(externalBMP);
+
+ Paint paintLine = new Paint();
+ paintLine.setAntiAlias(true);
+ paintLine.setDither(true);
+ paintLine.setStyle(Style.STROKE);
+ paintLine.setColor(Color.WHITE);
+ // internalCanvas.drawCircle(externalBMP.getWidth() / 2, externalBMP.getHeight() / 2, externalBMP.getWidth() / 2 - dpiPadding / 2, paintLine);
+ // internalCanvas.drawOval(new RectF(PADDING, PADDING, externalBMP.getWidth() - dpiPadding, externalBMP.getHeight() - dpiPadding), paint);
+ internalCanvas.drawOval(new RectF(0, 0, externalBMP.getWidth(), externalBMP.getHeight()), paint);
+
+ view.post(new Runnable() {
+ @Override
+ public void run() {
+ view.setImageBitmap(externalBMP);
+ contact.setPhoto(externalBMP);
+ view.invalidate();
+ }
+ });
+ }
+
+ public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
+
+ // First decode with inJustDecodeBounds=true to check dimensions
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ // options.inJustDecodeBounds = true;
+ // BitmapFactory.decodeResource(res, resId, options);
+
+ // Calculate inSampleSize
+ options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
+
+ // Decode bitmap with inSampleSize set
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeResource(res, resId, options);
+ }
+
+ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
+ // Raw height and width of image
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+
+ // Calculate ratios of height and width to requested height and width
+ final int heightRatio = Math.round((float) height / (float) reqHeight);
+ final int widthRatio = Math.round((float) width / (float) reqWidth);
+
+ // Choose the smallest ratio as inSampleSize value, this will guarantee
+ // a final image with both dimensions larger than or equal to the
+ // requested height and width.
+ inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
+ }
+
+ return inSampleSize;
+ }
+}
diff --git a/ring-android/src/cx/ring/adapters/ContactsAdapter.java b/ring-android/src/cx/ring/adapters/ContactsAdapter.java
new file mode 100644
index 0000000..de47d94
--- /dev/null
+++ b/ring-android/src/cx/ring/adapters/ContactsAdapter.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.adapters;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import cx.ring.R;
+import cx.ring.fragments.ContactListFragment;
+import cx.ring.model.CallContact;
+import cx.ring.views.stickylistheaders.StickyListHeadersAdapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class ContactsAdapter extends BaseAdapter implements StickyListHeadersAdapter, SectionIndexer {
+
+ private ExecutorService infos_fetcher = Executors.newCachedThreadPool();
+ Context mContext;
+
+ private ArrayList<CallContact> mContacts;
+ private int[] mSectionIndices;
+ private Character[] mSectionLetters;
+ WeakReference<ContactListFragment> parent;
+ private LayoutInflater mInflater;
+
+ // private static final String TAG = ContactsAdapter.class.getSimpleName();
+
+ public ContactsAdapter(ContactListFragment contactListFragment) {
+ super();
+ mContext = contactListFragment.getActivity();
+ mInflater = LayoutInflater.from(mContext);
+ parent = new WeakReference<ContactListFragment>(contactListFragment);
+ mContacts = new ArrayList<CallContact>();
+ mSectionIndices = getSectionIndices();
+ mSectionLetters = getSectionLetters();
+ }
+
+ public static final int TYPE_HEADER = 0;
+ public static final int TYPE_CONTACT = 1;
+
+ private int[] getSectionIndices() {
+ ArrayList<Integer> sectionIndices = new ArrayList<Integer>();
+ if (mContacts.isEmpty())
+ return new int[0];
+ char lastFirstChar = mContacts.get(0).getmDisplayName().charAt(0);
+ sectionIndices.add(0);
+ for (int i = 1; i < mContacts.size(); i++) {
+ if (mContacts.get(i).getmDisplayName().charAt(0) != lastFirstChar) {
+ lastFirstChar = mContacts.get(i).getmDisplayName().charAt(0);
+ sectionIndices.add(i);
+ }
+ }
+ int[] sections = new int[sectionIndices.size()];
+ for (int i = 0; i < sectionIndices.size(); i++) {
+ sections[i] = sectionIndices.get(i);
+ }
+ return sections;
+ }
+
+ private Character[] getSectionLetters() {
+ Character[] letters = new Character[mSectionIndices.length];
+ for (int i = 0; i < mSectionIndices.length; i++) {
+ letters[i] = mContacts.get(mSectionIndices[i]).getmDisplayName().charAt(0);
+ }
+ return letters;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup root) {
+ ContactView entryView;
+
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.item_contact, null);
+
+ entryView = new ContactView();
+ entryView.quick_starred = (ImageButton) convertView.findViewById(R.id.quick_starred);
+ entryView.quick_edit = (ImageButton) convertView.findViewById(R.id.quick_edit);
+ entryView.quick_discard = (ImageButton) convertView.findViewById(R.id.quick_discard);
+ entryView.quick_call = (ImageButton) convertView.findViewById(R.id.quick_call);
+ entryView.quick_msg = (ImageButton) convertView.findViewById(R.id.quick_message);
+ entryView.photo = (ImageView) convertView.findViewById(R.id.photo);
+ entryView.display_name = (TextView) convertView.findViewById(R.id.display_name);
+ convertView.setTag(entryView);
+ } else {
+ entryView = (ContactView) convertView.getTag();
+ }
+
+ final CallContact item = mContacts.get(position);
+
+ entryView.display_name.setText(item.getmDisplayName());
+
+ if (item.hasPhoto()) {
+ entryView.photo.setImageBitmap(item.getPhoto());
+ } else {
+ infos_fetcher.execute(new ContactPictureTask(mContext, entryView.photo, item));
+ }
+
+ entryView.quick_call.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ parent.get().mCallbacks.onCallContact(item);
+
+ }
+ });
+
+ entryView.quick_msg.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ parent.get().mCallbacks.onTextContact(item);
+ }
+ });
+
+ entryView.quick_starred.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Toast.makeText(mContext, "Coming soon", Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ entryView.quick_edit.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ parent.get().mCallbacks.onEditContact(item);
+
+ }
+ });
+
+ entryView.quick_discard.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Toast.makeText(mContext, "Coming soon", Toast.LENGTH_SHORT).show();
+
+ }
+ });
+
+ entryView.quick_edit.setClickable(false);
+ entryView.quick_discard.setClickable(false);
+ entryView.quick_starred.setClickable(false);
+
+ return convertView;
+ }
+
+ /*********************
+ * ViewHolder Pattern
+ *********************/
+ public class ContactView {
+ ImageButton quick_starred, quick_edit, quick_discard, quick_call, quick_msg;
+ ImageView photo;
+ TextView display_name;
+ }
+
+ @Override
+ public int getCount() {
+ return mContacts.size();
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getHeaderView(int position, View convertView, ViewGroup parent) {
+ HeaderViewHolder holder;
+
+ if (convertView == null) {
+ holder = new HeaderViewHolder();
+ convertView = mInflater.inflate(R.layout.header, parent, false);
+ holder.text = (TextView) convertView.findViewById(R.id.header_letter);
+ convertView.setTag(holder);
+ } else {
+ holder = (HeaderViewHolder) convertView.getTag();
+ }
+
+ // set header text as first char in name
+ char headerChar = mContacts.get(position).getmDisplayName().subSequence(0, 1).charAt(0);
+
+ holder.text.setText("" + headerChar);
+
+ return convertView;
+
+ }
+
+ class HeaderViewHolder {
+ TextView text;
+ }
+
+ @Override
+ public long getHeaderId(int position) {
+ // return the first character of the name as ID because this is what
+ // headers are based upon
+ return mContacts.get(position).getmDisplayName().subSequence(0, 1).charAt(0);
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ if (section >= mSectionIndices.length) {
+ section = mSectionIndices.length - 1;
+ } else if (section < 0) {
+ section = 0;
+ }
+ return mSectionIndices[section];
+ }
+
+ @Override
+ public int getSectionForPosition(int position) {
+ for (int i = 0; i < mSectionIndices.length; i++) {
+ if (position < mSectionIndices[i]) {
+ return i - 1;
+ }
+ }
+ return mSectionIndices.length - 1;
+ }
+
+ @Override
+ public Object[] getSections() {
+ return mSectionLetters;
+ }
+
+ @Override
+ public CallContact getItem(int position) {
+ return mContacts.get(position);
+ }
+
+ public void clear() {
+ mContacts = new ArrayList<CallContact>();
+ mSectionIndices = new int[0];
+ mSectionLetters = new Character[0];
+ notifyDataSetChanged();
+ }
+
+ public void restore() {
+ mContacts = new ArrayList<CallContact>();
+ mSectionIndices = getSectionIndices();
+ mSectionLetters = getSectionLetters();
+ notifyDataSetChanged();
+ }
+
+ public void addAll(ArrayList<CallContact> tmp) {
+ mContacts.addAll(tmp);
+ mSectionIndices = getSectionIndices();
+ mSectionLetters = getSectionLetters();
+ notifyDataSetChanged();
+ }
+
+}
diff --git a/ring-android/src/cx/ring/adapters/DiscussArrayAdapter.java b/ring-android/src/cx/ring/adapters/DiscussArrayAdapter.java
new file mode 100644
index 0000000..9cdfc41
--- /dev/null
+++ b/ring-android/src/cx/ring/adapters/DiscussArrayAdapter.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.adapters;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import cx.ring.R;
+import cx.ring.model.SipMessage;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class DiscussArrayAdapter extends BaseAdapter {
+
+ private TextView countryName;
+ private List<SipMessage> messages = new ArrayList<SipMessage>();
+ private LinearLayout wrapper;
+ private Context mContext;
+
+ public DiscussArrayAdapter(Context context, Bundle args) {
+ mContext = context;
+
+ if(args == null)
+ messages = new ArrayList<SipMessage>();
+ else
+ messages = args.getParcelableArrayList("messages");
+
+ }
+
+ public void add(SipMessage object) {
+ messages.add(object);
+ notifyDataSetChanged();
+ }
+
+ public int getCount() {
+ return this.messages.size();
+ }
+
+ public SipMessage getItem(int index) {
+ return this.messages.get(index);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View row = convertView;
+ if (row == null) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ row = inflater.inflate(R.layout.item_message, parent, false);
+ }
+
+ wrapper = (LinearLayout) row.findViewById(R.id.wrapper);
+
+ SipMessage coment = getItem(position);
+
+ countryName = (TextView) row.findViewById(R.id.comment);
+
+ countryName.setText(coment.comment);
+
+ countryName.setBackgroundResource(coment.left ? R.drawable.bubble_left_selector : R.drawable.bubble_right_selector);
+ wrapper.setGravity(coment.left ? Gravity.LEFT : Gravity.RIGHT);
+
+ return row;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/adapters/SectionsPagerAdapter.java b/ring-android/src/cx/ring/adapters/SectionsPagerAdapter.java
new file mode 100644
index 0000000..e9a4e15
--- /dev/null
+++ b/ring-android/src/cx/ring/adapters/SectionsPagerAdapter.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.adapters;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import cx.ring.R;
+import cx.ring.fragments.CallListFragment;
+import cx.ring.fragments.DialingFragment;
+import cx.ring.fragments.HistoryFragment;
+import cx.ring.views.PagerSlidingTabStrip;
+
+import android.content.Context;
+import android.util.Log;
+
+public class SectionsPagerAdapter extends android.support.v4.app.FragmentStatePagerAdapter implements PagerSlidingTabStrip.IconTabProvider {
+
+ private static final String TAG = SectionsPagerAdapter.class.getSimpleName();
+ Context mContext;
+ ArrayList<Fragment> fragments;
+
+ public SectionsPagerAdapter(Context c, FragmentManager fm) {
+ super(fm);
+ mContext = c;
+ fragments = new ArrayList<Fragment>();
+ fragments.add(new DialingFragment());
+ fragments.add(new CallListFragment());
+ fragments.add(new HistoryFragment());
+ }
+
+ @Override
+ public Fragment getItem(int i) {
+
+ return fragments.get(i);
+ }
+
+ public String getClassName(int i) {
+ String name;
+
+ switch (i) {
+ case 0:
+ name = DialingFragment.class.getName();
+ break;
+ case 1:
+ name = CallListFragment.class.getName();
+ break;
+ case 2:
+ name = HistoryFragment.class.getName();
+ break;
+
+ default:
+ Log.e(TAG, "getClassName: unknown fragment position " + i);
+ return null;
+ }
+
+ // Log.w(TAG, "getClassName: name=" + name);
+ return name;
+ }
+
+ @Override
+ public int getCount() {
+ return fragments.size();
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+
+ switch (position) {
+ case 0:
+ return mContext.getString(R.string.title_section0).toUpperCase(Locale.getDefault());
+ case 1:
+ return mContext.getString(R.string.title_section1).toUpperCase(Locale.getDefault());
+ case 2:
+ return mContext.getString(R.string.title_section2).toUpperCase(Locale.getDefault());
+ default:
+ Log.e(TAG, "getPageTitle: unknown tab position " + position);
+ break;
+ }
+ return null;
+ }
+
+ @Override
+ public int getPageIconResId(int position) {
+ switch (position) {
+ case 0:
+ return R.drawable.ic_action_dial_pad_light;
+ case 1:
+ return R.drawable.ic_action_call;
+ case 2:
+ return R.drawable.ic_action_time;
+ default:
+ Log.e(TAG, "getPageTitle: unknown tab position " + position);
+ break;
+ }
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/adapters/StarredContactsAdapter.java b/ring-android/src/cx/ring/adapters/StarredContactsAdapter.java
new file mode 100644
index 0000000..6690341
--- /dev/null
+++ b/ring-android/src/cx/ring/adapters/StarredContactsAdapter.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.adapters;
+
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import cx.ring.R;
+import cx.ring.model.CallContact;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class StarredContactsAdapter extends BaseAdapter {
+
+ private ExecutorService infos_fetcher = Executors.newCachedThreadPool();
+ private ArrayList<CallContact> dataset;
+ Context mContext;
+
+// private static final String TAG = ContactsAdapter.class.getSimpleName();
+
+ public StarredContactsAdapter(Context context) {
+ super();
+ mContext = context;
+ dataset = new ArrayList<CallContact>();
+ }
+
+ public void removeAll() {
+ dataset.clear();
+ notifyDataSetChanged();
+ }
+
+ public void addAll(ArrayList<CallContact> arrayList) {
+ dataset.addAll(arrayList);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return dataset.size();
+ }
+
+ @Override
+ public CallContact getItem(int index) {
+ return dataset.get(index);
+ }
+
+ @Override
+ public long getItemId(int index) {
+ return dataset.get(index).getId();
+ }
+
+ @Override
+ public View getView(int pos, View convView, ViewGroup parent) {
+
+ View v = convView;
+ LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ if (v == null) {
+ v = inflater.inflate(R.layout.item_contact_starred, null);
+ }
+
+ CallContact item = dataset.get(pos);
+
+ ((TextView) v.findViewById(R.id.display_name)).setText(item.getmDisplayName());
+ ImageView photo_view = (ImageView) v.findViewById(R.id.photo);
+
+ if(item.hasPhoto()){
+ photo_view.setImageBitmap(item.getPhoto());
+ } else {
+ infos_fetcher.execute(new ContactPictureTask(mContext, photo_view, item));
+ }
+ return v;
+ }
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/client/AccountEditionActivity.java b/ring-android/src/cx/ring/client/AccountEditionActivity.java
new file mode 100644
index 0000000..6433049
--- /dev/null
+++ b/ring-android/src/cx/ring/client/AccountEditionActivity.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ * Alexandre Savard <alexandre.savard@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.client;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.*;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.support.v13.app.FragmentStatePagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import cx.ring.R;
+import cx.ring.fragments.AdvancedAccountFragment;
+import cx.ring.fragments.AudioManagementFragment;
+import cx.ring.fragments.NestedSettingsFragment;
+import cx.ring.fragments.SecurityAccountFragment;
+import cx.ring.model.account.Account;
+import cx.ring.service.ISipService;
+import cx.ring.service.SipService;
+import cx.ring.views.PagerSlidingTabStrip;
+
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.Observable;
+import java.util.Observer;
+
+import cx.ring.fragments.GeneralAccountFragment;
+
+public class AccountEditionActivity extends Activity implements GeneralAccountFragment.Callbacks, AudioManagementFragment.Callbacks,
+ AdvancedAccountFragment.Callbacks, SecurityAccountFragment.Callbacks, NestedSettingsFragment.Callbacks {
+ private static final String TAG = AccountEditionActivity.class.getSimpleName();
+
+ private boolean mBound = false;
+ private ISipService service;
+ private Account acc_selected;
+
+ private NestedSettingsFragment toDisplay;
+
+ private Observer mAccountObserver = new Observer() {
+
+ @Override
+ public void update(Observable observable, Object data) {
+ processAccount();
+ }
+ };
+
+ PreferencesPagerAdapter mPreferencesPagerAdapter;
+ private ServiceConnection mConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder binder) {
+ service = ISipService.Stub.asInterface(binder);
+ mBound = true;
+
+ ArrayList<Fragment> fragments = new ArrayList<Fragment>();
+ if (acc_selected.isIP2IP()) {
+ fragments.add(new AudioManagementFragment());
+ } else {
+ fragments.add(new GeneralAccountFragment());
+ fragments.add(new AudioManagementFragment());
+ if(acc_selected.isSip())
+ {
+ fragments.add(new AdvancedAccountFragment());
+ fragments.add(new SecurityAccountFragment());
+ }
+ }
+
+ ViewPager mViewPager = (ViewPager) findViewById(R.id.pager);
+
+ mPreferencesPagerAdapter = new PreferencesPagerAdapter(AccountEditionActivity.this, getFragmentManager(), fragments);
+ mViewPager.setAdapter(mPreferencesPagerAdapter);
+ mViewPager.setOffscreenPageLimit(3);
+
+ final PagerSlidingTabStrip strip = PagerSlidingTabStrip.class.cast(findViewById(R.id.pager_sliding_strip));
+
+ strip.setViewPager(mViewPager);
+
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_account_settings);
+
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ acc_selected = getIntent().getExtras().getParcelable("account");
+
+ acc_selected.addObserver(mAccountObserver);
+
+ if (!mBound) {
+ Log.i(TAG, "onCreate: Binding service...");
+ Intent intent = new Intent(this, SipService.class);
+ bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (acc_selected.isIP2IP()) {
+ return true;
+ }
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.account_edition, menu);
+ return true;
+ }
+
+ @Override
+ public void onBackPressed() {
+
+ if (toDisplay != null) {
+ getFragmentManager().beginTransaction().setCustomAnimations(R.animator.slidein_up, R.animator.slideout_down).remove(toDisplay).commit();
+ ((SecurityAccountFragment) mPreferencesPagerAdapter.getItem(3)).updateSummaries();
+ toDisplay = null;
+ return;
+ }
+
+ if (acc_selected.isIP2IP()) {
+ super.onBackPressed();
+ return;
+ }
+
+ super.onBackPressed();
+
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mBound) {
+ unbindService(mConnection);
+ mBound = false;
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ if (toDisplay != null) {
+ getFragmentManager().beginTransaction().setCustomAnimations(R.animator.slidein_up, R.animator.slideout_down).remove(toDisplay)
+ .commit();
+ ((SecurityAccountFragment) mPreferencesPagerAdapter.getItem(3)).updateSummaries();
+ toDisplay = null;
+ } else
+ finish();
+ return true;
+ case R.id.menuitem_delete:
+ AlertDialog dialog = createDeleteDialog();
+ dialog.show();
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ private void processAccount() {
+ try {
+ service.setCredentials(acc_selected.getAccountID(), acc_selected.getCredentialsHashMapList());
+ service.setAccountDetails(acc_selected.getAccountID(), acc_selected.getDetails());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ private AlertDialog createDeleteDialog() {
+ Activity ownerActivity = this;
+ AlertDialog.Builder builder = new AlertDialog.Builder(ownerActivity);
+ builder.setMessage("Do you really want to delete this account").setTitle("Delete Account")
+ .setPositiveButton("Ok", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ Bundle bundle = new Bundle();
+ bundle.putString("AccountID", acc_selected.getAccountID());
+
+ try {
+ service.removeAccount(acc_selected.getAccountID());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ finish();
+ }
+ }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ /* Terminate with no action */
+ }
+ });
+
+ AlertDialog alertDialog = builder.create();
+ alertDialog.setOwnerActivity(ownerActivity);
+
+ return alertDialog;
+ }
+
+ public class PreferencesPagerAdapter extends FragmentStatePagerAdapter {
+
+ Context mContext;
+ ArrayList<Fragment> fragments;
+
+ public PreferencesPagerAdapter(Context c, FragmentManager fm, ArrayList<Fragment> items) {
+ super(fm);
+ mContext = c;
+ fragments = new ArrayList<Fragment>(items);
+
+ }
+
+ @Override
+ public int getCount() {
+ return fragments.size();
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ return fragments.get(position);
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case 0:
+ if (acc_selected.isIP2IP()) {
+ return getString(R.string.account_preferences_audio_tab).toUpperCase(Locale.getDefault());
+ } else {
+ return getString(R.string.account_preferences_basic_tab).toUpperCase(Locale.getDefault());
+ }
+ case 1:
+ return getString(R.string.account_preferences_audio_tab).toUpperCase(Locale.getDefault());
+ case 2:
+ return getString(R.string.account_preferences_advanced_tab).toUpperCase(Locale.getDefault());
+ case 3:
+ return getString(R.string.account_preferences_security_tab).toUpperCase(Locale.getDefault());
+ default:
+ Log.e(TAG, "getPreferencePageTitle: unknown tab position " + position);
+ break;
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public ISipService getService() {
+ return service;
+ }
+
+ @Override
+ public Account getAccount() {
+ return acc_selected;
+ }
+
+ @Override
+ public void displayCredentialsScreen() {
+ toDisplay = new NestedSettingsFragment();
+ Bundle b = new Bundle();
+ b.putInt("MODE", 0);
+ toDisplay.setArguments(b);
+ getFragmentManager().beginTransaction().setCustomAnimations(R.animator.slidein_up, R.animator.slideout_down)
+ .replace(R.id.hidden_container, toDisplay).commit();
+ }
+
+ @Override
+ public void displaySRTPScreen() {
+ toDisplay = new NestedSettingsFragment();
+ Bundle b = new Bundle();
+ b.putInt("MODE", 1);
+ toDisplay.setArguments(b);
+ getFragmentManager().beginTransaction().setCustomAnimations(R.animator.slidein_up, R.animator.slideout_down)
+ .replace(R.id.hidden_container, toDisplay).commit();
+ }
+
+ @Override
+ public void displayTLSScreen() {
+ toDisplay = new NestedSettingsFragment();
+ Bundle b = new Bundle();
+ b.putInt("MODE", 2);
+ toDisplay.setArguments(b);
+ getFragmentManager().beginTransaction().setCustomAnimations(R.animator.slidein_up, R.animator.slideout_down)
+ .replace(R.id.hidden_container, toDisplay).commit();
+ }
+
+}
diff --git a/ring-android/src/cx/ring/client/AccountWizard.java b/ring-android/src/cx/ring/client/AccountWizard.java
new file mode 100644
index 0000000..43cd2ba
--- /dev/null
+++ b/ring-android/src/cx/ring/client/AccountWizard.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.client;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v13.app.FragmentStatePagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.util.Log;
+import android.view.MenuItem;
+import cx.ring.R;
+import cx.ring.fragments.AccountCreationFragment;
+import cx.ring.service.ISipService;
+import cx.ring.service.SipService;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+public class AccountWizard extends Activity implements AccountCreationFragment.Callbacks {
+ static final String TAG = "AccountWizard";
+ private boolean mBound = false;
+ private ISipService service;
+ ViewPager mViewPager;
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder binder) {
+ service = ISipService.Stub.asInterface(binder);
+ mBound = true;
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_wizard);
+ mViewPager = (ViewPager) findViewById(R.id.pager);
+
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ getActionBar().setHomeButtonEnabled(true);
+ SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(AccountWizard.this, getFragmentManager());
+ mViewPager.setAdapter(mSectionsPagerAdapter);
+
+ if (!mBound) {
+ Log.i(TAG, "onCreate: Binding service...");
+ Intent intent = new Intent(this, SipService.class);
+ bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ }
+
+ /* activity finishes itself or is being killed by the system */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mBound) {
+ unbindService(mConnection);
+ mBound = false;
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ default:
+ return true;
+ }
+ }
+
+ public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
+
+ Context mContext;
+ ArrayList<Fragment> fragments;
+
+ public SectionsPagerAdapter(Context c, FragmentManager fm) {
+ super(fm);
+ mContext = c;
+ fragments = new ArrayList<Fragment>();
+ fragments.add(new AccountCreationFragment());
+
+ }
+
+ @Override
+ public Fragment getItem(int i) {
+
+ return fragments.get(i);
+ }
+
+ public String getClassName(int i) {
+ String name;
+
+ switch (i) {
+ case 0:
+ name = AccountCreationFragment.class.getName();
+ break;
+
+ default:
+ Log.e(TAG, "getClassName: unknown fragment position " + i);
+ return null;
+ }
+
+ // Log.w(TAG, "getClassName: name=" + name);
+ return name;
+ }
+
+ @Override
+ public int getCount() {
+ return 1;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case 0:
+ return mContext.getString(R.string.title_section0).toUpperCase(Locale.getDefault());
+ default:
+ Log.e(TAG, "getPageTitle: unknown tab position " + position);
+ break;
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public ISipService getService() {
+ return service;
+ }
+
+}
diff --git a/ring-android/src/cx/ring/client/CallActivity.java b/ring-android/src/cx/ring/client/CallActivity.java
new file mode 100644
index 0000000..8f664cd
--- /dev/null
+++ b/ring-android/src/cx/ring/client/CallActivity.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.client;
+
+import java.util.*;
+
+import android.support.v4.app.FragmentActivity;
+import android.util.Log;
+import cx.ring.R;
+import cx.ring.fragments.CallFragment;
+import cx.ring.fragments.IMFragment;
+import cx.ring.model.account.Account;
+import cx.ring.model.CallContact;
+import cx.ring.model.Conference;
+import cx.ring.model.SipCall;
+import cx.ring.model.SipMessage;
+import cx.ring.service.ISipService;
+import cx.ring.service.SipService;
+import cx.ring.utils.CallProximityManager;
+import cx.ring.views.CallPaneLayout;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.v4.widget.SlidingPaneLayout;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+public class CallActivity extends FragmentActivity implements IMFragment.Callbacks, CallFragment.Callbacks, CallProximityManager.ProximityDirector {
+
+ @SuppressWarnings("unused")
+ static final String TAG = "CallActivity";
+ private ISipService mService;
+ CallPaneLayout mSlidingPaneLayout;
+
+ IMFragment mIMFragment;
+ CallFragment mCurrentCallFragment;
+ private Conference mDisplayedConference;
+
+ /* result code sent in case of call failure */
+ public static int RESULT_FAILURE = -10;
+ private CallProximityManager mProximityManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_call_layout);
+
+ Window w = getWindow();
+ w.setFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED, WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+
+ setUpSlidingPanel();
+
+ mProximityManager = new CallProximityManager(this, this);
+ mProximityManager.startTracking();
+
+ mCurrentCallFragment = new CallFragment();
+ mIMFragment = new IMFragment();
+
+ if(!checkExternalCall()) {
+ mDisplayedConference = getIntent().getParcelableExtra("conference");
+ Bundle IMBundle = new Bundle();
+ if (getIntent().getBooleanExtra("resuming", false)) {
+ IMBundle.putParcelableArrayList("messages", mDisplayedConference.getMessages());
+ mIMFragment.setArguments(IMBundle);
+ } else {
+ IMBundle.putParcelableArrayList("messages", new ArrayList<SipMessage>());
+ mIMFragment.setArguments(IMBundle);
+ }
+ }
+
+ mSlidingPaneLayout.setCurFragment(mCurrentCallFragment);
+ getSupportFragmentManager().beginTransaction().replace(R.id.ongoingcall_pane, mCurrentCallFragment)
+ .replace(R.id.message_list_frame, mIMFragment).commit();
+ }
+
+ private void setUpSlidingPanel() {
+ mSlidingPaneLayout = (CallPaneLayout) findViewById(R.id.slidingpanelayout);
+ mSlidingPaneLayout.setParallaxDistance(500);
+ mSlidingPaneLayout.setSliderFadeColor(Color.TRANSPARENT);
+
+ mSlidingPaneLayout.setPanelSlideListener(new SlidingPaneLayout.PanelSlideListener() {
+
+ @Override
+ public void onPanelSlide(View view, float offSet) {
+ }
+
+ @Override
+ public void onPanelOpened(View view) {
+ getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
+ }
+
+ @Override
+ public void onPanelClosed(View view) {
+ mCurrentCallFragment.getBubbleView().restartDrawing();
+ getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
+ }
+ });
+ }
+
+ @Override
+ public void onFragmentCreated() {
+ Intent intent = new Intent(this, SipService.class);
+ bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ Window window = getWindow();
+ window.setFormat(PixelFormat.RGBA_8888);
+ }
+
+ private Handler mHandler = new Handler();
+ private Runnable mUpdateTimeTask = new Runnable() {
+ @Override
+ public void run() {
+ if (mCurrentCallFragment != null)
+ mCurrentCallFragment.updateTime();
+ mHandler.postAtTime(this, SystemClock.uptimeMillis() + 1000);
+ }
+ };
+
+ /* activity no more in foreground */
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mHandler.removeCallbacks(mUpdateTimeTask);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ return super.onKeyUp(keyCode, event);
+ }
+ mCurrentCallFragment.onKeyUp(keyCode, event);
+ return true;
+ }
+
+ @Override
+ protected void onDestroy() {
+
+ unbindService(mConnection);
+
+ mProximityManager.stopTracking();
+ mProximityManager.release(0);
+
+ super.onDestroy();
+ }
+
+ /**
+ * Defines callbacks for service binding, passed to bindService()
+ */
+ private ServiceConnection mConnection = new ServiceConnection() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder binder) {
+ mService = ISipService.Stub.asInterface(binder);
+
+ if (mDisplayedConference.getState().contentEquals("NONE")) {
+ try {
+ mService.placeCall(mDisplayedConference.getParticipants().get(0));
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+ }
+ };
+
+ private boolean checkExternalCall() {
+ Uri u = getIntent().getData();
+ if (u != null) {
+ CallContact c = CallContact.ContactBuilder.buildUnknownContact(u.getSchemeSpecificPart());
+ try {
+ String accountID = (String) mService.getAccountList().get(1); // We use the first account to place outgoing calls
+ HashMap<String, String> details = (HashMap<String, String>) mService.getAccountDetails(accountID);
+ ArrayList<HashMap<String, String>> credentials = (ArrayList<HashMap<String, String>>) mService.getCredentials(accountID);
+ Account acc = new Account(accountID, details, credentials);
+
+ Bundle args = new Bundle();
+ args.putString(SipCall.ID, Integer.toString(Math.abs(new Random().nextInt())));
+ args.putParcelable(SipCall.ACCOUNT, acc);
+ args.putInt(SipCall.STATE, SipCall.state.CALL_STATE_NONE);
+ args.putInt(SipCall.TYPE, SipCall.direction.CALL_TYPE_OUTGOING);
+ args.putParcelable(SipCall.CONTACT, c);
+
+ mDisplayedConference = new Conference(Conference.DEFAULT_ID);
+ mDisplayedConference.getParticipants().add(new SipCall(args));
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public ISipService getService() {
+ return mService;
+ }
+
+ @Override
+ public Conference getDisplayedConference() {
+ return mDisplayedConference;
+ }
+
+ @Override
+ public void updateDisplayedConference(Conference c) {
+ if(mDisplayedConference.equals(c)){
+ mDisplayedConference = c;
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ Intent launchHome = new Intent(this, HomeActivity.class);
+ launchHome.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ launchHome.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ startActivity(launchHome);
+ }
+
+ @Override
+ public void terminateCall() {
+ mHandler.removeCallbacks(mUpdateTimeTask);
+ mCurrentCallFragment.getBubbleView().stopThread();
+ TimerTask quit = new TimerTask() {
+
+ @Override
+ public void run() {
+ finish();
+ }
+ };
+
+ new Timer().schedule(quit, 1000);
+ }
+
+ @Override
+ public boolean sendIM(SipMessage msg) {
+
+ try {
+ Log.i(TAG, "Sending:"+msg.comment+"to"+mDisplayedConference.getId());
+ mService.sendTextMessage(mDisplayedConference.getId(), msg);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void startTimer() {
+ mHandler.postDelayed(mUpdateTimeTask, 0);
+ }
+
+ @Override
+ public void slideChatScreen() {
+
+ if (mSlidingPaneLayout.isOpen()) {
+ mSlidingPaneLayout.closePane();
+ } else {
+ mCurrentCallFragment.getBubbleView().stopThread();
+ mSlidingPaneLayout.openPane();
+ }
+ }
+
+ @Override
+ public boolean shouldActivateProximity() {
+ return true;
+ }
+
+ @Override
+ public void onProximityTrackingChanged(boolean acquired) {
+ // TODO Stub de la méthode généré automatiquement
+
+ }
+}
diff --git a/ring-android/src/cx/ring/client/DetailHistoryActivity.java b/ring-android/src/cx/ring/client/DetailHistoryActivity.java
new file mode 100644
index 0000000..89625f4
--- /dev/null
+++ b/ring-android/src/cx/ring/client/DetailHistoryActivity.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.client;
+
+import cx.ring.R;
+import cx.ring.fragments.DetailsHistoryEntryFragment;
+import cx.ring.fragments.HistoryFragment;
+import cx.ring.model.Conference;
+import cx.ring.model.SipCall;
+import cx.ring.service.ISipService;
+import cx.ring.service.SipService;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.MenuItem;
+
+public class DetailHistoryActivity extends Activity implements DetailsHistoryEntryFragment.Callbacks {
+
+ private boolean mBound = false;
+ private ISipService service;
+ private String TAG = DetailHistoryActivity.class.getSimpleName();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_holder);
+
+ Intent intent = new Intent(this, SipService.class);
+ bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ default:
+ return true;
+ }
+ }
+
+ @Override
+ public ISipService getService() {
+ return service;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mBound) {
+ unbindService(mConnection);
+ mBound = false;
+ }
+ }
+
+ /** Defines callbacks for service binding, passed to bindService() */
+ private ServiceConnection mConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder binder) {
+ service = ISipService.Stub.asInterface(binder);
+
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+
+ Fragment fr = new DetailsHistoryEntryFragment();
+ fr.setArguments(getIntent().getBundleExtra(HistoryFragment.ARGS));
+ ft.replace(R.id.frag_container, fr);
+
+ ft.commit();
+
+ mBound = true;
+ Log.d(TAG, "Service connected service=" + service);
+
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+
+ mBound = false;
+ Log.d(TAG, "Service disconnected service=" + service);
+ }
+ };
+
+ @Override
+ public void onCall(SipCall call) {
+ Bundle bundle = new Bundle();
+ Conference tmp = new Conference(Conference.DEFAULT_ID);
+
+ tmp.getParticipants().add(call);
+
+ bundle.putParcelable("conference", tmp);
+ Intent intent = new Intent().setClass(this, CallActivity.class);
+ intent.putExtra("resuming", false);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/client/HomeActivity.java b/ring-android/src/cx/ring/client/HomeActivity.java
new file mode 100644
index 0000000..fdcb022
--- /dev/null
+++ b/ring-android/src/cx/ring/client/HomeActivity.java
@@ -0,0 +1,651 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Adrien Beraud <adrien.beraud@gmail.com>
+ * Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.client;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Random;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.support.v4.app.*;
+import cx.ring.R;
+import cx.ring.fragments.AboutFragment;
+import cx.ring.fragments.AccountsManagementFragment;
+import cx.ring.fragments.CallListFragment;
+import cx.ring.fragments.ContactListFragment;
+import cx.ring.fragments.DialingFragment;
+import cx.ring.fragments.HistoryFragment;
+import cx.ring.fragments.HomeFragment;
+import cx.ring.fragments.MenuFragment;
+import cx.ring.history.HistoryEntry;
+import cx.ring.model.account.Account;
+import cx.ring.model.CallContact;
+import cx.ring.model.Conference;
+import cx.ring.model.SipCall;
+import cx.ring.service.ISipService;
+import cx.ring.service.SipService;
+import cx.ring.views.SlidingUpPanelLayout;
+import cx.ring.views.SlidingUpPanelLayout.PanelSlideListener;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.support.v4.view.GravityCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.RelativeLayout;
+import android.widget.Toast;
+
+public class HomeActivity extends FragmentActivity implements DialingFragment.Callbacks, AccountsManagementFragment.Callbacks,
+ ContactListFragment.Callbacks, CallListFragment.Callbacks, HistoryFragment.Callbacks, MenuFragment.Callbacks {
+
+ static final String TAG = HomeActivity.class.getSimpleName();
+
+ private ContactListFragment mContactsFragment = null;
+ private MenuFragment fMenu;
+
+ private boolean mBound = false;
+ private ISipService service;
+
+ public static final int REQUEST_CODE_PREFERENCES = 1;
+ public static final int REQUEST_CODE_CALL = 3;
+
+ SlidingUpPanelLayout mContactDrawer;
+ private DrawerLayout mNavigationDrawer;
+ private ActionBarDrawerToggle mDrawerToggle;
+
+ private boolean isClosing = false;
+ private Timer t = new Timer();
+
+ protected Fragment fContent;
+
+ /* called before activity is killed, e.g. rotation */
+ @Override
+ protected void onSaveInstanceState(Bundle bundle) {
+ super.onSaveInstanceState(bundle);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_home);
+
+ // Bind to LocalService
+ if (!mBound) {
+ Log.i(TAG, "onStart: Binding service...");
+ Intent intent = new Intent(this, SipService.class);
+ startService(intent);
+ bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ mContactsFragment = new ContactListFragment();
+ getFragmentManager().beginTransaction().replace(R.id.contacts_frame, mContactsFragment).commit();
+
+ mContactDrawer = (SlidingUpPanelLayout) findViewById(R.id.contact_panel);
+ // mContactDrawer.setShadowDrawable(getResources().getDrawable(R.drawable.above_shadow));
+ mContactDrawer.setAnchorPoint(0.3f);
+ mContactDrawer.setDragView(findViewById(R.id.contacts_frame));
+ mContactDrawer.setEnableDragViewTouchEvents(true);
+ mContactDrawer.setPanelSlideListener(new PanelSlideListener() {
+
+ @Override
+ public void onPanelSlide(View panel, float slideOffset) {
+ if (slideOffset < 0.2) {
+ if (getActionBar().isShowing()) {
+ getActionBar().hide();
+ }
+ } else {
+ if (!getActionBar().isShowing()) {
+ getActionBar().show();
+ }
+ }
+ }
+
+ @Override
+ public void onPanelExpanded(View panel) {
+
+ }
+
+ @Override
+ public void onPanelCollapsed(View panel) {
+
+ }
+
+ @Override
+ public void onPanelAnchored(View panel) {
+
+ }
+ });
+
+ mNavigationDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
+
+ // set a custom shadow that overlays the main content when the drawer opens
+ mNavigationDrawer.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
+
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ getActionBar().setHomeButtonEnabled(true);
+
+ mDrawerToggle = new ActionBarDrawerToggle(this, /* host Activity */
+ mNavigationDrawer, /* DrawerLayout object */
+ R.drawable.ic_drawer, /* nav drawer image to replace 'Up' caret */
+ R.string.drawer_open, /* "open drawer" description for accessibility */
+ R.string.drawer_close /* "close drawer" description for accessibility */
+ ) {
+ @Override
+ public void onDrawerClosed(View view) {
+ invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
+ }
+
+ @Override
+ public void onDrawerOpened(View drawerView) {
+ // getActionBar().setTitle(mDrawerTitle);
+ invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
+ }
+ };
+
+ mNavigationDrawer.setDrawerListener(mDrawerToggle);
+
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ // Sync the toggle state after onRestoreInstanceState has occurred.
+ mDrawerToggle.syncState();
+ if (mContactDrawer.isExpanded()) {
+ getActionBar().hide();
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mDrawerToggle.onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ protected void onStart() {
+ Log.i(TAG, "onStart");
+
+ if (!PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getBoolean("installed", false)) {
+ PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).edit().putBoolean("installed", true).commit();
+
+ copyAssetFolder(getAssets(), "ringtones", getFilesDir().getAbsolutePath() + "/ringtones");
+ }
+
+ super.onStart();
+
+ }
+
+ private static boolean copyAssetFolder(AssetManager assetManager, String fromAssetPath, String toPath) {
+ try {
+ String[] files = assetManager.list(fromAssetPath);
+ new File(toPath).mkdirs();
+ Log.i(TAG, "Creating :" + toPath);
+ boolean res = true;
+ for (String file : files)
+ if (file.contains(".")) {
+ Log.i(TAG, "Copying file :" + fromAssetPath + "/" + file + " to " + toPath + "/" + file);
+ res &= copyAsset(assetManager, fromAssetPath + "/" + file, toPath + "/" + file);
+ } else {
+ Log.i(TAG, "Copying folder :" + fromAssetPath + "/" + file + " to " + toPath + "/" + file);
+ res &= copyAssetFolder(assetManager, fromAssetPath + "/" + file, toPath + "/" + file);
+ }
+ return res;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ private static boolean copyAsset(AssetManager assetManager, String fromAssetPath, String toPath) {
+ InputStream in;
+ OutputStream out;
+ try {
+ in = assetManager.open(fromAssetPath);
+ new File(toPath).createNewFile();
+ out = new FileOutputStream(toPath);
+ copyFile(in, out);
+ in.close();
+ in = null;
+ out.flush();
+ out.close();
+ out = null;
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ private static void copyFile(InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[1024];
+ int read;
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+ }
+
+ /* user gets back to the activity, e.g. through task manager */
+ @Override
+ protected void onRestart() {
+ super.onRestart();
+ }
+
+ /* activity gets back to the foreground and user input */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public void onBackPressed() {
+
+ if (mNavigationDrawer.isDrawerVisible(Gravity.LEFT)) {
+ mNavigationDrawer.closeDrawer(Gravity.LEFT);
+ return;
+ }
+
+ if (mContactDrawer.isExpanded() || mContactDrawer.isAnchored()) {
+ mContactDrawer.collapsePane();
+ return;
+ }
+
+ if (getSupportFragmentManager().getBackStackEntryCount() > 1) {
+ popCustomBackStack();
+ fMenu.backToHome();
+ return;
+ }
+
+ if (isClosing) {
+ super.onBackPressed();
+ t.cancel();
+ finish();
+ } else {
+ t.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ isClosing = false;
+ }
+ }, 3000);
+ Toast.makeText(this, getResources().getString(R.string.close_msg), Toast.LENGTH_SHORT).show();
+ isClosing = true;
+ }
+ }
+
+ private void popCustomBackStack() {
+ FragmentManager fm = getSupportFragmentManager();
+ FragmentManager.BackStackEntry entry = fm.getBackStackEntryAt(0);
+ fContent = fm.findFragmentByTag(entry.getName());
+ for (int i = 0; i < fm.getBackStackEntryCount() - 1; ++i) {
+ fm.popBackStack();
+ }
+ }
+
+ /* activity no more in foreground */
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ if (mBound) {
+ unbindService(mConnection);
+ mBound = false;
+ }
+ }
+
+ /* activity finishes itself or is being killed by the system */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ Log.i(TAG, "onDestroy: destroying service...");
+ Intent sipServiceIntent = new Intent(this, SipService.class);
+ stopService(sipServiceIntent);
+ }
+
+ public void launchCallActivity(SipCall infos) {
+ Conference tmp = new Conference(Conference.DEFAULT_ID);
+
+ tmp.getParticipants().add(infos);
+ Intent intent = new Intent().setClass(this, CallActivity.class);
+ intent.putExtra("conference", tmp);
+ intent.putExtra("resuming", false);
+ startActivityForResult(intent, REQUEST_CODE_CALL);
+
+ // overridePendingTransition(R.anim.slide_down, R.anim.slide_up);
+ }
+
+ /**
+ * Defines callbacks for service binding, passed to bindService()
+ */
+ private ServiceConnection mConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder binder) {
+ service = ISipService.Stub.asInterface(binder);
+ fMenu = new MenuFragment();
+ fContent = new HomeFragment();
+ getSupportFragmentManager().beginTransaction().replace(R.id.left_drawer, fMenu).replace(R.id.main_frame, fContent, "Home").addToBackStack("Home").commit();
+ mBound = true;
+ Log.d(TAG, "Service connected service=" + service);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+
+ mBound = false;
+ Log.d(TAG, "Service disconnected service=" + service);
+ }
+ };
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ Log.i(TAG, "onOptionsItemSelected " + item.getItemId());
+
+ if (mDrawerToggle.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ switch (requestCode) {
+ case REQUEST_CODE_PREFERENCES:
+ case AccountsManagementFragment.ACCOUNT_EDIT_REQUEST:
+ if (fMenu != null)
+ fMenu.updateAllAccounts();
+ break;
+ case REQUEST_CODE_CALL:
+ if (resultCode == CallActivity.RESULT_FAILURE) {
+ Log.w(TAG, "Call Failed");
+ }
+ break;
+ }
+
+ }
+
+ @Override
+ public ISipService getService() {
+ return service;
+ }
+
+ @Override
+ public void onTextContact(final CallContact c) {
+ // TODO
+ }
+
+ @Override
+ public void onEditContact(final CallContact c) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, String.valueOf(c.getId()));
+ intent.setData(uri);
+ startActivity(intent);
+ }
+
+ @Override
+ public void onCallContact(final CallContact c) {
+
+ if (fMenu.getSelectedAccount() == null) {
+ createAccountDialog().show();
+ return;
+ }
+
+ if (!fMenu.getSelectedAccount().isRegistered()) {
+ createNotRegisteredDialog().show();
+ return;
+ }
+
+ getActionBar().show();
+ Thread launcher = new Thread(new Runnable() {
+
+ final String[] CONTACTS_PHONES_PROJECTION = new String[]{Phone.NUMBER, Phone.TYPE};
+ final String[] CONTACTS_SIP_PROJECTION = new String[]{SipAddress.SIP_ADDRESS, SipAddress.TYPE};
+
+ @Override
+ public void run() {
+
+ Bundle args = new Bundle();
+ args.putString(SipCall.ID, Integer.toString(Math.abs(new Random().nextInt())));
+ args.putParcelable(SipCall.ACCOUNT, fMenu.getSelectedAccount());
+ args.putInt(SipCall.STATE, SipCall.state.CALL_STATE_NONE);
+ args.putInt(SipCall.TYPE, SipCall.direction.CALL_TYPE_OUTGOING);
+
+ Cursor cPhones = getContentResolver().query(Phone.CONTENT_URI, CONTACTS_PHONES_PROJECTION, Phone.CONTACT_ID + " =" + c.getId(),
+ null, null);
+
+ while (cPhones.moveToNext()) {
+ c.addPhoneNumber(cPhones.getString(cPhones.getColumnIndex(Phone.NUMBER)), cPhones.getInt(cPhones.getColumnIndex(Phone.TYPE)));
+ }
+ cPhones.close();
+
+ Cursor cSip = getContentResolver().query(Phone.CONTENT_URI, CONTACTS_SIP_PROJECTION, Phone.CONTACT_ID + "=" + c.getId(), null,
+ null);
+
+ while (cSip.moveToNext()) {
+ c.addSipNumber(cSip.getString(cSip.getColumnIndex(SipAddress.SIP_ADDRESS)), cSip.getInt(cSip.getColumnIndex(SipAddress.TYPE)));
+ }
+ cSip.close();
+
+ args.putParcelable(SipCall.CONTACT, c);
+
+ launchCallActivity(new SipCall(args));
+ }
+ });
+ launcher.start();
+ mContactDrawer.collapsePane();
+
+ }
+
+ @Override
+ public void onCallHistory(HistoryEntry to) {
+
+ Account usedAccount = fMenu.retrieveAccountById(to.getAccountID());
+
+ if (usedAccount == null) {
+ createAccountDialog().show();
+ return;
+ }
+
+ if (usedAccount.isRegistered()) {
+ Bundle args = new Bundle();
+ args.putString(SipCall.ID, Integer.toString(Math.abs(new Random().nextInt())));
+ args.putParcelable(SipCall.ACCOUNT, usedAccount);
+ args.putInt(SipCall.STATE, SipCall.state.CALL_STATE_NONE);
+ args.putInt(SipCall.TYPE, SipCall.direction.CALL_TYPE_OUTGOING);
+ args.putParcelable(SipCall.CONTACT, to.getContact());
+
+ try {
+ launchCallActivity(new SipCall(args));
+ } catch (Exception e) {
+ Log.e(TAG, e.toString());
+ }
+ } else {
+ createNotRegisteredDialog().show();
+ }
+ }
+
+ @Override
+ public void onCallDialed(String to) {
+ Account usedAccount = fMenu.getSelectedAccount();
+
+ if (usedAccount == null) {
+ createAccountDialog().show();
+ return;
+ }
+
+ if (fMenu.getSelectedAccount().isRegistered()) {
+ Bundle args = new Bundle();
+ args.putString(SipCall.ID, Integer.toString(Math.abs(new Random().nextInt())));
+ args.putParcelable(SipCall.ACCOUNT, usedAccount);
+ args.putInt(SipCall.STATE, SipCall.state.CALL_STATE_NONE);
+ args.putInt(SipCall.TYPE, SipCall.direction.CALL_TYPE_OUTGOING);
+ args.putParcelable(SipCall.CONTACT, CallContact.ContactBuilder.buildUnknownContact(to));
+
+ try {
+ launchCallActivity(new SipCall(args));
+ } catch (Exception e) {
+ Log.e(TAG, e.toString());
+ }
+ } else {
+ createNotRegisteredDialog().show();
+ }
+ }
+
+ private AlertDialog createNotRegisteredDialog() {
+ final Activity ownerActivity = this;
+ AlertDialog.Builder builder = new AlertDialog.Builder(ownerActivity);
+
+ builder.setMessage(getResources().getString(R.string.cannot_pass_sipcall))
+ .setTitle(getResources().getString(R.string.cannot_pass_sipcall_title))
+ .setPositiveButton(getResources().getString(android.R.string.ok), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+
+ }
+ });
+
+ AlertDialog alertDialog = builder.create();
+ alertDialog.setOwnerActivity(ownerActivity);
+
+ return alertDialog;
+ }
+
+ private AlertDialog createAccountDialog() {
+ final Activity ownerActivity = this;
+ AlertDialog.Builder builder = new AlertDialog.Builder(ownerActivity);
+
+ builder.setMessage(getResources().getString(R.string.create_new_account_dialog))
+ .setTitle(getResources().getString(R.string.create_new_account_dialog_title))
+ .setPositiveButton(getResources().getString(android.R.string.ok), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ Intent in = new Intent();
+ in.setClass(ownerActivity, AccountWizard.class);
+ ownerActivity.startActivityForResult(in, HomeActivity.REQUEST_CODE_PREFERENCES);
+ }
+ }).setNegativeButton(getResources().getString(android.R.string.cancel), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ dialog.dismiss();
+ }
+ });
+
+ AlertDialog alertDialog = builder.create();
+ alertDialog.setOwnerActivity(ownerActivity);
+
+ return alertDialog;
+ }
+
+ @Override
+ public void onContactDragged() {
+ mContactDrawer.collapsePane();
+ }
+
+ @Override
+ public void toggleDrawer() {
+ if (mContactDrawer.isAnchored())
+ mContactDrawer.expandPane();
+ else if (!mContactDrawer.isExpanded())
+ mContactDrawer.expandPane(0.3f);
+ else
+ mContactDrawer.collapsePane();
+ }
+
+ @Override
+ public void toggleForSearchDrawer() {
+ if (mContactDrawer.isExpanded()) {
+ mContactDrawer.collapsePane();
+ } else
+ mContactDrawer.expandPane();
+ }
+
+ @Override
+ public void setDragView(RelativeLayout relativeLayout) {
+ mContactDrawer.setDragView(relativeLayout);
+ }
+
+ @Override
+ public void onSectionSelected(int pos) {
+
+ mNavigationDrawer.closeDrawers();
+
+ switch (pos) {
+ case 0:
+
+ if (fContent instanceof HomeFragment)
+ break;
+
+ if (getSupportFragmentManager().getBackStackEntryCount() == 1)
+ break;
+
+ popCustomBackStack();
+
+ break;
+ case 1:
+ if (fContent instanceof AccountsManagementFragment)
+ break;
+ fContent = new AccountsManagementFragment();
+ getSupportFragmentManager().beginTransaction().setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE).replace(R.id.main_frame, fContent, "Accounts").addToBackStack("Accounts").commit();
+ break;
+ case 2:
+ if (fContent instanceof AboutFragment)
+ break;
+ fContent = new AboutFragment();
+ getSupportFragmentManager().beginTransaction().setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE).replace(R.id.main_frame, fContent, "About").addToBackStack("About").commit();
+ break;
+ }
+
+
+ }
+
+}
diff --git a/ring-android/src/cx/ring/fragments/AboutFragment.java b/ring-android/src/cx/ring/fragments/AboutFragment.java
new file mode 100644
index 0000000..be1037c
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/AboutFragment.java
@@ -0,0 +1,42 @@
+package cx.ring.fragments;
+
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.text.Html;
+import android.text.method.LinkMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import cx.ring.R;
+
+public class AboutFragment extends Fragment {
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ getActivity().getActionBar().setTitle(R.string.menu_item_about);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
+ View inflatedView = inflater.inflate(R.layout.frag_about, parent, false);
+
+ TextView title = (TextView) inflatedView.findViewById(R.id.app_name);
+ try {
+ String versionName = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0).versionName;
+ title.setText(getString(R.string.app_name) + " - " + versionName);
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+
+ TextView link = (TextView) inflatedView.findViewById(R.id.web_site);
+ String linkText = "<a href='http://sflphone.org/'>" + getResources().getString(R.string.web_site) + "</a>";
+ link.setText(Html.fromHtml(linkText));
+ link.setMovementMethod(LinkMovementMethod.getInstance());
+
+ return inflatedView;
+ }
+
+}
diff --git a/ring-android/src/cx/ring/fragments/AccountCreationFragment.java b/ring-android/src/cx/ring/fragments/AccountCreationFragment.java
new file mode 100644
index 0000000..c6c272a
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/AccountCreationFragment.java
@@ -0,0 +1,213 @@
+package cx.ring.fragments;
+
+import java.util.HashMap;
+
+import cx.ring.R;
+import cx.ring.model.account.AccountDetailBasic;
+import cx.ring.client.HomeActivity;
+import cx.ring.service.ISipService;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
+import cx.ring.views.PasswordEditText;
+
+public class AccountCreationFragment extends Fragment {
+
+ // Values for email and password at the time of the login attempt.
+ private String mAlias;
+ private String mHostname;
+ private String mUsername;
+ private String mPassword;
+ private String mAccountType;
+
+ // UI references.
+ private EditText mAliasView;
+ private EditText mHostnameView;
+ private EditText mUsernameView;
+ private PasswordEditText mPasswordView;
+ private Spinner mAccountTypeView;
+
+ private Callbacks mCallbacks = sDummyCallbacks;
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+
+ @Override
+ public ISipService getService() {
+ return null;
+ }
+ };
+
+ public interface Callbacks {
+
+ public ISipService getService();
+
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
+ View inflatedView = inflater.inflate(R.layout.frag_account_creation, parent, false);
+
+ mAliasView = (EditText) inflatedView.findViewById(R.id.alias);
+ mHostnameView = (EditText) inflatedView.findViewById(R.id.hostname);
+ mUsernameView = (EditText) inflatedView.findViewById(R.id.username);
+ mPasswordView = (PasswordEditText) inflatedView.findViewById(R.id.password);
+ mAccountTypeView = (Spinner) inflatedView.findViewById(R.id.account_type);
+
+ mPasswordView.getEdit_text().setOnEditorActionListener(new OnEditorActionListener() {
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ // if(actionId == EditorInfo.IME_ACTION_GO || event.getAction() == KeyEvent.KEYCODE_ENTER){
+ mAlias = mAliasView.getText().toString();
+ mHostname = mHostnameView.getText().toString();
+ mUsername = mUsernameView.getText().toString();
+ mPassword = mPasswordView.getText().toString();
+ mAccountType = mAccountTypeView.getSelectedItem().toString();
+ attemptCreation();
+ // }
+
+ return true;
+ }
+ });
+ inflatedView.findViewById(R.id.create_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mAlias = mAliasView.getText().toString();
+ mHostname = mHostnameView.getText().toString();
+ mUsername = mUsernameView.getText().toString();
+ mPassword = mPasswordView.getText().toString();
+ mAccountType = mAccountTypeView.getSelectedItem().toString();
+ attemptCreation();
+ }
+ });
+
+ return inflatedView;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+ }
+
+ /**
+ * Attempts to sign in or register the account specified by the login form. If there are form errors (invalid email, missing fields, etc.), the
+ * errors are presented and no actual login attempt is made.
+ */
+ public void attemptCreation() {
+
+ // Reset errors.
+ mAliasView.setError(null);
+ mPasswordView.setError(null);
+
+ // Store values at the time of the login attempt.
+
+ boolean cancel = false;
+ View focusView = null;
+
+ // Check for a valid password.
+ if (TextUtils.isEmpty(mPassword)) {
+ mPasswordView.setError(getString(R.string.error_field_required));
+ focusView = mPasswordView;
+ cancel = true;
+ }
+
+ if (TextUtils.isEmpty(mUsername)) {
+ mUsernameView.setError(getString(R.string.error_field_required));
+ focusView = mUsernameView;
+ cancel = true;
+ }
+
+ if (TextUtils.isEmpty(mHostname)) {
+ mHostnameView.setError(getString(R.string.error_field_required));
+ focusView = mHostnameView;
+ cancel = true;
+ }
+
+ // Check for a valid email address.
+ if (TextUtils.isEmpty(mAlias)) {
+ mAliasView.setError(getString(R.string.error_field_required));
+ focusView = mAliasView;
+ cancel = true;
+ }
+
+ if (cancel) {
+ // There was an error; don't attempt login and focus the first
+ // form field with an error.
+ focusView.requestFocus();
+ } else {
+ // Show a progress spinner, and kick off a background task to
+ // perform the user login attempt.
+ initCreation();
+
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void initCreation() {
+
+ try {
+ HashMap<String, String> accountDetails = (HashMap<String, String>) mCallbacks.getService().getAccountTemplate();
+ accountDetails.put(AccountDetailBasic.CONFIG_ACCOUNT_ALIAS, mAlias);
+ accountDetails.put(AccountDetailBasic.CONFIG_ACCOUNT_HOSTNAME, mHostname);
+ accountDetails.put(AccountDetailBasic.CONFIG_ACCOUNT_USERNAME, mUsername);
+ accountDetails.put(AccountDetailBasic.CONFIG_ACCOUNT_PASSWORD, mPassword);
+ accountDetails.put(AccountDetailBasic.CONFIG_ACCOUNT_TYPE, mAccountType);
+
+ createNewAccount(accountDetails);
+
+ } catch (RemoteException e) {
+ Toast.makeText(getActivity(), "Error creating account", Toast.LENGTH_SHORT).show();
+ e.printStackTrace();
+ }
+
+ Intent resultIntent = new Intent(getActivity(), HomeActivity.class);
+ getActivity().setResult(Activity.RESULT_OK, resultIntent);
+ resultIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ startActivity(resultIntent);
+ getActivity().finish();
+
+ }
+
+ private void createNewAccount(HashMap<String, String> accountDetails) {
+ try {
+
+ mCallbacks.getService().addAccount(accountDetails);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/ring-android/src/cx/ring/fragments/AccountWrapperFragment.java b/ring-android/src/cx/ring/fragments/AccountWrapperFragment.java
new file mode 100644
index 0000000..6172708
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/AccountWrapperFragment.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.fragments;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import cx.ring.interfaces.AccountsInterface;
+import cx.ring.service.ConfigurationManagerCallback;
+
+public abstract class AccountWrapperFragment extends Fragment implements AccountsInterface {
+
+
+ private AccountsReceiver mReceiver;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mReceiver = new AccountsReceiver();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(ConfigurationManagerCallback.ACCOUNT_STATE_CHANGED);
+ intentFilter.addAction(ConfigurationManagerCallback.ACCOUNTS_CHANGED);
+ getActivity().registerReceiver(mReceiver, intentFilter);
+ }
+
+ @Override
+ public void accountsChanged() {
+
+ }
+
+ @Override
+ public void accountStateChanged(String accoundID, String state, int code) {
+
+ }
+
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ getActivity().unregisterReceiver(mReceiver);
+ }
+
+ public class AccountsReceiver extends BroadcastReceiver {
+
+ private final String TAG = AccountsReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().contentEquals(ConfigurationManagerCallback.ACCOUNT_STATE_CHANGED)) {
+ Log.i(TAG, "Received" + intent.getAction());
+ accountStateChanged(intent.getStringExtra("Account"), intent.getStringExtra("state"), intent.getIntExtra("code", 0));
+ } else if (intent.getAction().contentEquals(ConfigurationManagerCallback.ACCOUNTS_CHANGED)) {
+ Log.i(TAG, "Received" + intent.getAction());
+ accountsChanged();
+
+ }
+
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/fragments/AccountsManagementFragment.java b/ring-android/src/cx/ring/fragments/AccountsManagementFragment.java
new file mode 100644
index 0000000..09da403
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/AccountsManagementFragment.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
+ * Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.fragments;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.AsyncTaskLoader;
+import android.support.v4.content.Loader;
+import android.util.Log;
+import android.view.*;
+import android.view.View.OnClickListener;
+import android.widget.*;
+import android.widget.AdapterView.OnItemClickListener;
+import cx.ring.R;
+import cx.ring.client.AccountEditionActivity;
+import cx.ring.client.AccountWizard;
+import cx.ring.loaders.AccountsLoader;
+import cx.ring.loaders.LoaderConstants;
+import cx.ring.model.account.Account;
+import cx.ring.service.ISipService;
+import cx.ring.views.dragsortlv.DragSortListView;
+
+import java.io.File;
+import java.util.ArrayList;
+
+public class AccountsManagementFragment extends AccountWrapperFragment implements LoaderManager.LoaderCallbacks<Bundle> {
+ static final String TAG = "AccountManagementFragment";
+ static final String DEFAULT_ACCOUNT_ID = "IP2IP";
+ static final int ACCOUNT_CREATE_REQUEST = 1;
+ public static final int ACCOUNT_EDIT_REQUEST = 2;
+ AccountsAdapter mAccountsAdapter;
+ AccountsAdapter mIP2IPAdapter;
+
+ DragSortListView mDnDListView;
+ private View mLoadingView;
+ private int mShortAnimationDuration;
+
+ private DragSortListView.DropListener onDrop = new DragSortListView.DropListener() {
+ @Override
+ public void drop(int from, int to) {
+ if (from != to) {
+ Account item = mAccountsAdapter.getItem(from);
+ mAccountsAdapter.remove(item);
+ mAccountsAdapter.insert(item, to);
+ try {
+ mCallbacks.getService().setAccountOrder(mAccountsAdapter.generateAccountOrder());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ };
+
+ private Callbacks mCallbacks = sDummyCallbacks;
+ private Account ip2ip;
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+
+ @Override
+ public ISipService getService() {
+ return null;
+ }
+ };
+ private AccountsLoader accountsLoader;
+
+ public interface Callbacks {
+
+ public ISipService getService();
+
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Log.i(TAG, "Create Account Management Fragment");
+ mAccountsAdapter = new AccountsAdapter(getActivity(), new ArrayList<Account>());
+ mIP2IPAdapter = new AccountsAdapter(getActivity(), new ArrayList<Account>());
+ this.setHasOptionsMenu(true);
+
+ mShortAnimationDuration = getResources().getInteger(android.R.integer.config_mediumAnimTime);
+ Log.i(TAG, "anim time: " + mShortAnimationDuration);
+ getLoaderManager().initLoader(LoaderConstants.ACCOUNTS_LOADER, null, this);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
+ View inflatedView = inflater.inflate(R.layout.frag_accounts_list, parent, false);
+ ((ListView)inflatedView.findViewById(R.id.accounts_list)).setAdapter(mAccountsAdapter);
+
+ return inflatedView;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ mDnDListView = (DragSortListView) getView().findViewById(R.id.accounts_list);
+
+ mDnDListView.setDropListener(onDrop);
+ mDnDListView.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1, int pos, long arg3) {
+ launchAccountEditActivity(mAccountsAdapter.getItem(pos));
+ }
+ });
+
+ ((ListView) getView().findViewById(R.id.ip2ip)).setAdapter(mIP2IPAdapter);
+ ((ListView) getView().findViewById(R.id.ip2ip)).setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
+ launchAccountEditActivity(ip2ip);
+
+ }
+ });
+
+ mLoadingView = view.findViewById(R.id.loading_spinner);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ }
+
+ public void onResume() {
+ super.onResume();
+ accountsLoader.onContentChanged();
+ getActivity().getActionBar().setTitle(R.string.menu_item_accounts);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu m, MenuInflater inf) {
+ super.onCreateOptionsMenu(m, inf);
+ inf.inflate(R.menu.account_creation, m);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ super.onOptionsItemSelected(item);
+ switch (item.getItemId()) {
+ case R.id.menuitem_create:
+ Intent intent = new Intent().setClass(getActivity(), AccountWizard.class);
+ startActivityForResult(intent, ACCOUNT_CREATE_REQUEST);
+ break;
+ }
+
+ return true;
+ }
+
+ private void launchAccountEditActivity(Account acc) {
+ Log.i(TAG, "Launch account edit activity");
+
+ Intent intent = new Intent().setClass(getActivity(), AccountEditionActivity.class);
+ Bundle bundle = new Bundle();
+ bundle.putParcelable("account", acc);
+
+ intent.putExtras(bundle);
+
+ startActivityForResult(intent, ACCOUNT_EDIT_REQUEST);
+ }
+
+ @Override
+ public void accountsChanged() {
+ accountsLoader.onContentChanged();
+ }
+
+ @Override
+ public void accountStateChanged(String accoundID, String state, int code) {
+ mAccountsAdapter.updateAccount(accoundID, state, code);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ accountsLoader.onContentChanged();
+ }
+
+ /**
+ * Adapter for accounts List
+ *
+ * @author lisional
+ */
+ public class AccountsAdapter extends BaseAdapter {
+
+ // private static final String TAG = AccountSelectionAdapter.class.getSimpleName();
+
+ ArrayList<Account> accounts;
+ Context mContext;
+
+ public AccountsAdapter(Context cont, ArrayList<Account> newList) {
+ super();
+ accounts = newList;
+ mContext = cont;
+ }
+
+ public void insert(Account item, int to) {
+ accounts.add(to, item);
+ notifyDataSetChanged();
+ }
+
+ public void remove(Account item) {
+ accounts.remove(item);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ return accounts.size();
+ }
+
+ @Override
+ public Account getItem(int pos) {
+ return accounts.get(pos);
+ }
+
+ @Override
+ public long getItemId(int pos) {
+ return 0;
+ }
+
+ @Override
+ public View getView(final int pos, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ AccountView entryView;
+
+ if (rowView == null) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ rowView = inflater.inflate(R.layout.item_account_pref, null);
+
+ entryView = new AccountView();
+ entryView.alias = (TextView) rowView.findViewById(R.id.account_alias);
+ entryView.host = (TextView) rowView.findViewById(R.id.account_host);
+ entryView.enabled = (CheckBox) rowView.findViewById(R.id.account_checked);
+ rowView.setTag(entryView);
+ } else {
+ entryView = (AccountView) rowView.getTag();
+ }
+
+ final Account item = accounts.get(pos);
+ entryView.alias.setText(accounts.get(pos).getAlias());
+ if (item.isIP2IP()) {
+ entryView.host.setText(item.getRegistered_state());
+ entryView.enabled.setVisibility(View.GONE);
+ } else {
+ entryView.host.setText(item.getHost() + " - " + item.getRegistered_state());
+ entryView.enabled.setChecked(item.isEnabled());
+ entryView.enabled.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ item.setEnabled(!item.isEnabled());
+
+ try {
+ mCallbacks.getService().setAccountDetails(item.getAccountID(), item.getDetails());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ return rowView;
+ }
+
+ /**
+ * ******************
+ * ViewHolder Pattern
+ * *******************
+ */
+ public class AccountView {
+ public TextView alias;
+ public TextView host;
+ public CheckBox enabled;
+ }
+
+ public void removeAll() {
+ accounts.clear();
+ notifyDataSetChanged();
+
+ }
+
+ public void addAll(ArrayList<Account> results) {
+ accounts.addAll(results);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Modify state of specific account
+ */
+ public void updateAccount(String accoundID, String state, int code) {
+ Log.i(TAG, "updateAccount:" + state);
+ for (Account a : accounts) {
+ if (a.getAccountID().contentEquals(accoundID)) {
+ a.setRegistered_state(state);
+ notifyDataSetChanged();
+ return;
+ }
+ }
+
+ }
+
+ private String generateAccountOrder() {
+ String result = DEFAULT_ACCOUNT_ID + File.separator;
+ for (Account a : accounts) {
+ result += a.getAccountID() + File.separator;
+ }
+ return result;
+ }
+
+ }
+
+ private void crossfade() {
+
+ // Set the content view to 0% opacity but visible, so that it is visible
+ // (but fully transparent) during the animation.
+ mDnDListView.setAlpha(0f);
+ mDnDListView.setVisibility(View.VISIBLE);
+
+ // Animate the content view to 100% opacity, and clear any animation
+ // listener set on the view.
+ mDnDListView.animate().alpha(1f).setDuration(mShortAnimationDuration).setListener(null);
+
+ // Animate the loading view to 0% opacity. After the animation ends,
+ // set its visibility to GONE as an optimization step (it won't
+ // participate in layout passes, etc.)
+ mLoadingView.animate().alpha(0f).setDuration(mShortAnimationDuration).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mLoadingView.setVisibility(View.GONE);
+ }
+ });
+ }
+
+
+ @Override
+ public AsyncTaskLoader<Bundle> onCreateLoader(int arg0, Bundle arg1) {
+ accountsLoader = new AccountsLoader(getActivity(), mCallbacks.getService());
+ return accountsLoader;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Bundle> bundleLoader, Bundle results) {
+ mAccountsAdapter.removeAll();
+ ArrayList<Account> tmp = results.getParcelableArrayList(AccountsLoader.ACCOUNTS);
+ ip2ip = results.getParcelable(AccountsLoader.ACCOUNT_IP2IP);
+ mAccountsAdapter.addAll(tmp);
+ mIP2IPAdapter.removeAll();
+ mIP2IPAdapter.insert(ip2ip, 0);
+ if (mAccountsAdapter.isEmpty()) {
+ mDnDListView.setEmptyView(getView().findViewById(R.id.empty_account_list));
+ }
+ crossfade();
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Bundle> bundleLoader) {
+
+ }
+
+}
diff --git a/ring-android/src/cx/ring/fragments/AdvancedAccountFragment.java b/ring-android/src/cx/ring/fragments/AdvancedAccountFragment.java
new file mode 100644
index 0000000..71e7da4
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/AdvancedAccountFragment.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.fragments;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+
+import cx.ring.R;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.PreferenceFragment;
+import android.util.Log;
+import cx.ring.model.account.AccountDetail;
+import cx.ring.model.account.AccountDetailAdvanced;
+import cx.ring.model.account.Account;
+
+public class AdvancedAccountFragment extends PreferenceFragment {
+
+ private static final String TAG = AdvancedAccountFragment.class.getSimpleName();
+
+ private Callbacks mCallbacks = sDummyCallbacks;
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+
+ @Override
+ public Account getAccount() {
+ return null;
+ }
+
+ };
+
+ public interface Callbacks {
+
+ public Account getAccount();
+
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.account_advanced_prefs);
+ setPreferenceDetails(mCallbacks.getAccount().getAdvancedDetails());
+ addPreferenceListener(mCallbacks.getAccount().getAdvancedDetails(), changeAdvancedPreferenceListener);
+
+ }
+
+ private void setPreferenceDetails(AccountDetail details) {
+ for (AccountDetail.PreferenceEntry p : details.getDetailValues()) {
+ Log.i(TAG, "setPreferenceDetails: pref " + p.mKey + " value " + p.mValue);
+ Preference pref = findPreference(p.mKey);
+ if (pref != null) {
+ if (p.mKey == AccountDetailAdvanced.CONFIG_LOCAL_INTERFACE) {
+ ArrayList<CharSequence> entries = getNetworkInterfaces();
+ CharSequence[] display = new CharSequence[entries.size()];
+ entries.toArray(display);
+ ((ListPreference) pref).setEntries(display);
+ ((ListPreference) pref).setEntryValues(display);
+ pref.setSummary(p.mValue);
+ continue;
+ }
+ if (!p.isTwoState) {
+ pref.setSummary(p.mValue);
+ } else if (pref.getKey().contentEquals(AccountDetailAdvanced.CONFIG_STUN_ENABLE)) {
+ ((CheckBoxPreference) pref).setChecked(p.mValue.contentEquals("true"));
+ findPreference(AccountDetailAdvanced.CONFIG_STUN_SERVER).setEnabled(p.mValue.contentEquals("true"));
+ } else if (pref.getKey().contentEquals("Account.publishedSameAsLocal")) {
+ ((CheckBoxPreference) pref).setChecked(p.mValue.contentEquals("true"));
+ findPreference(AccountDetailAdvanced.CONFIG_PUBLISHED_PORT).setEnabled(!p.mValue.contentEquals("true"));
+ findPreference(AccountDetailAdvanced.CONFIG_PUBLISHED_ADDRESS).setEnabled(!p.mValue.contentEquals("true"));
+ }
+ } else {
+ Log.w(TAG, "pref not found");
+ }
+ }
+ }
+
+ private ArrayList<CharSequence> getNetworkInterfaces() {
+ ArrayList<CharSequence> result = new ArrayList<CharSequence>();
+ result.add("default");
+ try {
+
+ for (Enumeration<NetworkInterface> list = NetworkInterface.getNetworkInterfaces(); list.hasMoreElements();) {
+ NetworkInterface i = list.nextElement();
+ if (i.isUp())
+ result.add(i.getDisplayName());
+ }
+ } catch (SocketException e) {
+ Log.e(TAG, e.toString());
+ }
+ return result;
+ }
+
+ private void addPreferenceListener(AccountDetail details, OnPreferenceChangeListener listener) {
+ for (AccountDetail.PreferenceEntry p : details.getDetailValues()) {
+ Log.i(TAG, "addPreferenceListener: pref " + p.mKey + p.mValue);
+ Preference pref = findPreference(p.mKey);
+ if (pref != null) {
+
+ pref.setOnPreferenceChangeListener(listener);
+
+ } else {
+ Log.w(TAG, "addPreferenceListener: pref not found");
+ }
+ }
+ }
+
+ Preference.OnPreferenceChangeListener changeAdvancedPreferenceListener = new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+
+ if (preference instanceof CheckBoxPreference) {
+ mCallbacks.getAccount().getAdvancedDetails().setDetailString(preference.getKey(), newValue.toString());
+ if (preference.getKey().contentEquals(AccountDetailAdvanced.CONFIG_STUN_ENABLE)) {
+ findPreference(AccountDetailAdvanced.CONFIG_STUN_SERVER).setEnabled((Boolean) newValue);
+ } else if (preference.getKey().contentEquals(AccountDetailAdvanced.CONFIG_PUBLISHED_SAMEAS_LOCAL)) {
+ findPreference(AccountDetailAdvanced.CONFIG_PUBLISHED_PORT).setEnabled(!(Boolean) newValue);
+ findPreference(AccountDetailAdvanced.CONFIG_PUBLISHED_ADDRESS).setEnabled(!(Boolean) newValue);
+ }
+ } else {
+ Log.i(TAG, "Changing" + preference.getKey() + " value:" + newValue);
+ if(preference.getKey().contentEquals(AccountDetailAdvanced.CONFIG_AUDIO_PORT_MAX) ||
+ preference.getKey().contentEquals(AccountDetailAdvanced.CONFIG_AUDIO_PORT_MIN))
+ newValue = adjustRtpRange(Integer.valueOf((String) newValue));
+
+ preference.setSummary((CharSequence) newValue);
+ mCallbacks.getAccount().getAdvancedDetails().setDetailString(preference.getKey(), newValue.toString());
+ }
+
+ mCallbacks.getAccount().notifyObservers();
+ return true;
+ }
+ };
+
+ private String adjustRtpRange(int newValue) {
+ if(newValue < 1024)
+ return "1024";
+ if(newValue > 65534)
+ return "65534";
+ return String.valueOf(newValue);
+ }
+
+}
diff --git a/ring-android/src/cx/ring/fragments/AudioManagementFragment.java b/ring-android/src/cx/ring/fragments/AudioManagementFragment.java
new file mode 100644
index 0000000..71789e4
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/AudioManagementFragment.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
+ * Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.fragments;
+
+import java.io.File;
+import java.util.ArrayList;
+
+import android.content.Intent;
+import cx.ring.R;
+import cx.ring.model.account.AccountDetail;
+import cx.ring.model.account.AccountDetailAdvanced;
+import cx.ring.model.account.Account;
+import cx.ring.model.Codec;
+import cx.ring.service.ISipService;
+import cx.ring.views.dragsortlv.DragSortListView;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.PreferenceFragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class AudioManagementFragment extends PreferenceFragment {
+ static final String TAG = AudioManagementFragment.class.getSimpleName();
+
+ protected Callbacks mCallbacks = sDummyCallbacks;
+ ArrayList<Codec> codecs;
+ private DragSortListView mCodecList;
+ CodecAdapter listAdapter;
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+
+ @Override
+ public ISipService getService() {
+ return null;
+ }
+
+ @Override
+ public Account getAccount() {
+ return null;
+ }
+
+ };
+
+ public interface Callbacks {
+
+ public ISipService getService();
+
+ public Account getAccount();
+
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+ try {
+ codecs = (ArrayList<Codec>) mCallbacks.getService().getAudioCodecList(mCallbacks.getAccount().getAccountID());
+ mCallbacks.getService().getRingtoneList();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ private DragSortListView.DropListener onDrop = new DragSortListView.DropListener() {
+ @Override
+ public void drop(int from, int to) {
+ if (from != to) {
+ Codec item = listAdapter.getItem(from);
+ listAdapter.remove(item);
+ listAdapter.insert(item, to);
+ try {
+ mCallbacks.getService().setActiveCodecList(getActiveCodecList(), mCallbacks.getAccount().getAccountID());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ };
+
+ private ListView mPrefsList;
+
+ public ArrayList<String> getActiveCodecList() {
+ ArrayList<String> results = new ArrayList<String>();
+ for (int i = 0; i < listAdapter.getCount(); ++i) {
+ if (listAdapter.getItem(i).isEnabled()) {
+ results.add(listAdapter.getItem(i).getPayload().toString());
+ }
+ }
+ return results;
+ }
+
+ private static final int SELECT_RINGTONE_PATH = 40;
+ private Preference.OnPreferenceClickListener filePickerListener = new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ performFileSearch(SELECT_RINGTONE_PATH);
+ return true;
+ }
+ };
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == Activity.RESULT_CANCELED)
+ return;
+
+ File myFile = new File(data.getData().getPath());
+ Log.i(TAG, "file selected:" + data.getData());
+ if (requestCode == SELECT_RINGTONE_PATH) {
+ findPreference(AccountDetailAdvanced.CONFIG_RINGTONE_PATH).setSummary(myFile.getName());
+ mCallbacks.getAccount().getAdvancedDetails().setDetailString(AccountDetailAdvanced.CONFIG_RINGTONE_PATH, myFile.getAbsolutePath());
+ mCallbacks.getAccount().notifyObservers();
+ }
+
+ }
+
+ public void performFileSearch(int requestCodeToSet) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("audio/*");
+ startActivityForResult(intent, requestCodeToSet);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ View rootView = LayoutInflater.from(getActivity()).inflate(R.layout.frag_audio_mgmt, null);
+
+ mPrefsList = (ListView) rootView.findViewById(android.R.id.list);
+ mCodecList = (DragSortListView) rootView.findViewById(R.id.dndlistview);
+ mCodecList.setAdapter(listAdapter);
+ mCodecList.setDropListener(onDrop);
+ mCodecList.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1, int pos, long arg3) {
+ listAdapter.getItem(pos).toggleState();
+ listAdapter.notifyDataSetChanged();
+ try {
+ mCallbacks.getService().setActiveCodecList(getActiveCodecList(), mCallbacks.getAccount().getAccountID());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+
+ }
+ });
+ return rootView;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ final LinearLayout holder = (LinearLayout) getView().findViewById(R.id.lv_holder);
+ final LinearLayout holder_prefs = (LinearLayout) getView().findViewById(R.id.lv_holder_prefs);
+ holder.post(new Runnable() {
+
+ @Override
+ public void run() {
+ setListViewHeight(mCodecList, holder);
+ setListViewHeight(mPrefsList, holder_prefs);
+ }
+ });
+
+ }
+
+ // Sets the ListView holder's height
+ public void setListViewHeight(ListView listView, LinearLayout llMain) {
+ ListAdapter listAdapter = listView.getAdapter();
+ if (listAdapter == null) {
+ return;
+ }
+
+ int totalHeight = 0;
+ int firstHeight;
+ int desiredWidth = MeasureSpec.makeMeasureSpec(listView.getWidth(), MeasureSpec.AT_MOST);
+
+ for (int i = 0; i < listAdapter.getCount(); i++) {
+ View listItem = listAdapter.getView(i, null, listView);
+ listItem.measure(desiredWidth, MeasureSpec.UNSPECIFIED);
+ firstHeight = listItem.getMeasuredHeight();
+ totalHeight += firstHeight;
+ }
+
+ totalHeight += getView().findViewById(R.id.list_header_title).getMeasuredHeight();
+ LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) llMain.getLayoutParams();
+ params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount()));
+ llMain.setLayoutParams(params);
+ getView().requestLayout();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.account_audio_prefs);
+ listAdapter = new CodecAdapter(getActivity());
+ listAdapter.setDataset(codecs);
+
+ setPreferenceDetails(mCallbacks.getAccount().getAdvancedDetails());
+ findPreference(AccountDetailAdvanced.CONFIG_RINGTONE_PATH).setEnabled(
+ ((CheckBoxPreference) findPreference(AccountDetailAdvanced.CONFIG_RINGTONE_ENABLED)).isChecked());
+ addPreferenceListener(mCallbacks.getAccount().getAdvancedDetails(), changeAudioPreferenceListener);
+ }
+
+ Preference.OnPreferenceChangeListener changeAudioPreferenceListener = new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (preference instanceof CheckBoxPreference) {
+ if (preference.getKey().contentEquals(AccountDetailAdvanced.CONFIG_RINGTONE_ENABLED))
+ getPreferenceScreen().findPreference(AccountDetailAdvanced.CONFIG_RINGTONE_PATH).setEnabled((Boolean) newValue);
+ mCallbacks.getAccount().getAdvancedDetails().setDetailString(preference.getKey(), newValue.toString());
+ } else {
+ if (preference.getKey().contentEquals(AccountDetailAdvanced.CONFIG_ACCOUNT_DTMF_TYPE)) {
+ preference.setSummary(((String)newValue).contentEquals("overrtp") ? "RTP" : "SIP");
+ } else {
+ preference.setSummary((CharSequence) newValue);
+ Log.i(TAG, "Changing" + preference.getKey() + " value:" + newValue);
+ mCallbacks.getAccount().getAdvancedDetails().setDetailString(preference.getKey(), newValue.toString());
+ }
+ }
+ mCallbacks.getAccount().notifyObservers();
+
+ return true;
+ }
+ };
+
+ private void setPreferenceDetails(AccountDetail details) {
+ for (AccountDetail.PreferenceEntry p : details.getDetailValues()) {
+ Log.i(TAG, "setPreferenceDetails: pref " + p.mKey + " value " + p.mValue);
+ Preference pref = findPreference(p.mKey);
+ if (pref != null) {
+ if (!p.isTwoState) {
+ if (p.mKey.contentEquals(AccountDetailAdvanced.CONFIG_ACCOUNT_DTMF_TYPE)) {
+ pref.setDefaultValue(p.mValue.contentEquals("overrtp") ? "RTP" : "SIP");
+ pref.setSummary(p.mValue.contentEquals("overrtp") ? "RTP" : "SIP");
+ } else {
+ if(pref.getKey().contentEquals(AccountDetailAdvanced.CONFIG_RINGTONE_PATH)){
+ File tmp = new File(p.mValue);
+ pref.setSummary(tmp.getName());
+ } else
+ pref.setSummary(p.mValue);
+ }
+
+ } else {
+ ((CheckBoxPreference) pref).setChecked(p.mValue.contentEquals("true"));
+ }
+
+ } else {
+ Log.w(TAG, "pref not found");
+ }
+ }
+ }
+
+ private void addPreferenceListener(AccountDetail details, OnPreferenceChangeListener listener) {
+ for (AccountDetail.PreferenceEntry p : details.getDetailValues()) {
+ Log.i(TAG, "addPreferenceListener: pref " + p.mKey + p.mValue);
+ Preference pref = findPreference(p.mKey);
+ if (pref != null) {
+ pref.setOnPreferenceChangeListener(listener);
+ if (pref.getKey().contentEquals(AccountDetailAdvanced.CONFIG_RINGTONE_PATH))
+ pref.setOnPreferenceClickListener(filePickerListener);
+ } else {
+ Log.w(TAG, "addPreferenceListener: pref not found");
+ }
+ }
+ }
+
+ public static class CodecAdapter extends BaseAdapter {
+
+ ArrayList<Codec> items;
+ private Context mContext;
+
+ public CodecAdapter(Context context) {
+ items = new ArrayList<Codec>();
+ mContext = context;
+ }
+
+ public void insert(Codec item, int to) {
+ items.add(to, item);
+ notifyDataSetChanged();
+ }
+
+ public void remove(Codec item) {
+ items.remove(item);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return items.size();
+ }
+
+ @Override
+ public Codec getItem(int position) {
+ return items.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int pos, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ CodecView entryView;
+
+ if (rowView == null) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ rowView = inflater.inflate(R.layout.item_codec, null);
+
+ entryView = new CodecView();
+ entryView.name = (TextView) rowView.findViewById(R.id.codec_name);
+ entryView.samplerate = (TextView) rowView.findViewById(R.id.codec_samplerate);
+ entryView.enabled = (CheckBox) rowView.findViewById(R.id.codec_checked);
+ rowView.setTag(entryView);
+ } else {
+ entryView = (CodecView) rowView.getTag();
+ }
+
+ if (items.get(pos).isSpeex())
+ entryView.samplerate.setVisibility(View.VISIBLE);
+ else
+ entryView.samplerate.setVisibility(View.GONE);
+
+ entryView.name.setText(items.get(pos).getName());
+ entryView.samplerate.setText(items.get(pos).getSampleRate());
+ entryView.enabled.setChecked(items.get(pos).isEnabled());
+
+ return rowView;
+
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return getCount() == 0;
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+
+ public void setDataset(ArrayList<Codec> codecs) {
+ items = new ArrayList<Codec>(codecs);
+ }
+
+ /**
+ * ******************
+ * ViewHolder Pattern
+ * *******************
+ */
+ public class CodecView {
+ public TextView name;
+ public TextView samplerate;
+ public CheckBox enabled;
+ }
+ }
+}
diff --git a/ring-android/src/cx/ring/fragments/CallFragment.java b/ring-android/src/cx/ring/fragments/CallFragment.java
new file mode 100644
index 0000000..b2b2406
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/CallFragment.java
@@ -0,0 +1,772 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.fragments;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.PointF;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.RemoteException;
+import android.support.v4.app.FragmentManager;
+import android.util.FloatMath;
+import android.util.Log;
+import android.view.*;
+import android.view.SurfaceHolder.Callback;
+import android.view.View.OnClickListener;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.*;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import cx.ring.R;
+import cx.ring.interfaces.CallInterface;
+import cx.ring.service.ISipService;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+import cx.ring.model.Attractor;
+import cx.ring.model.Bubble;
+import cx.ring.model.BubbleContact;
+import cx.ring.model.BubbleModel;
+import cx.ring.model.BubbleUser;
+import cx.ring.model.BubblesView;
+import cx.ring.model.CallContact;
+import cx.ring.model.Conference;
+import cx.ring.model.SecureSipCall;
+import cx.ring.model.SipCall;
+
+public class CallFragment extends CallableWrapperFragment implements CallInterface, Callback {
+
+ static final String TAG = "CallFragment";
+
+
+
+ private float bubbleSize = 75; // dip
+ private float attractorSize = 40;
+ public static final int REQUEST_TRANSFER = 10;
+
+ // Screen wake lock for incoming call
+ private WakeLock mScreenWakeLock;
+
+ private BubblesView mBubbleView;
+ private BubbleModel mBubbleModel;
+
+ private Bitmap buttonCall;
+ private Bitmap buttonMsg;
+ private Bitmap buttonHold;
+ private Bitmap buttonUnhold;
+ private Bitmap buttonTransfer;
+ private Bitmap buttonHangUp;
+
+ private final int BTN_MSG_IDX = 0;
+ private final int BTN_HOLD_IDX = 1;
+ private final int BTN_TRANSFER_IDX = 2;
+ private final int BTN_HUNGUP_IDX = 3;
+
+ private BubbleModel.ActionGroup userActions;
+ private BubbleModel.ActionGroup callActions;
+
+ ViewSwitcher mSecuritySwitch;
+ private TextView mCallStatusTxt;
+ private ToggleButton mToggleSpeakers;
+
+ public Callbacks mCallbacks = sDummyCallbacks;
+ boolean accepted = false;
+
+ TransferDFragment editName;
+ private WifiManager wifiManager;
+ private BroadcastReceiver wifiReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ WifiInfo info = wifiManager.getConnectionInfo();
+ Log.i(TAG, "Level of wifi " + info.getRssi());
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedBundle) {
+ super.onCreate(savedBundle);
+
+ Resources r = getResources();
+
+ bubbleSize = r.getDimension(R.dimen.bubble_size);
+ attractorSize = r.getDimension(R.dimen.bubble_action_size);
+ float attractorMargin = r.getDimension(R.dimen.bubble_action_margin);
+
+ buttonCall = BitmapFactory.decodeResource(r, R.drawable.ic_action_call);
+ buttonMsg = BitmapFactory.decodeResource(r, R.drawable.ic_action_chat);
+ buttonHold = BitmapFactory.decodeResource(r, R.drawable.ic_action_pause_over_video);
+ buttonUnhold = BitmapFactory.decodeResource(r, R.drawable.ic_action_play_over_video);
+ buttonTransfer = BitmapFactory.decodeResource(r, R.drawable.ic_action_forward);
+ buttonHangUp = BitmapFactory.decodeResource(r, R.drawable.ic_action_end_call);
+
+ BubbleModel.ActionGroupCallback cb = new BubbleModel.ActionGroupCallback() {
+ @Override
+ public boolean onBubbleAction(Bubble b, int action) {
+ Log.i(TAG, "onBubbleAction ! "+action);
+ switch(action) {
+ case BTN_HUNGUP_IDX:
+ try {
+ if (b.isConference())
+ mCallbacks.getService().hangUpConference(b.getCallID());
+ else
+ mCallbacks.getService().hangUp(b.getCallID());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ return true;
+ case BTN_HOLD_IDX:
+ try {
+ if (b.getHoldStatus()) {
+ if (b.isConference())
+ mCallbacks.getService().unholdConference(b.getCallID());
+ else
+ mCallbacks.getService().unhold(b.getCallID());
+ } else {
+ if (b.isConference())
+ mCallbacks.getService().holdConference(b.getCallID());
+ else
+ mCallbacks.getService().hold(b.getCallID());
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return false;
+ case BTN_TRANSFER_IDX:
+ makeTransfer((BubbleContact) b);
+ return false;
+ }
+ return false;
+ }
+ };
+
+ userActions = new BubbleModel.ActionGroup(cb, attractorMargin, .4f, .25f);
+ userActions.addAction(BTN_HOLD_IDX, buttonHold, getString(R.string.action_call_hold), attractorSize);
+ userActions.addAction(BTN_HUNGUP_IDX, buttonHangUp, getString(R.string.action_call_hangup), attractorSize);
+
+ callActions = new BubbleModel.ActionGroup(cb, attractorMargin, .4f, .25f);
+ callActions.addAction(BTN_HOLD_IDX, buttonHold, getString(R.string.action_call_hold), attractorSize);
+ callActions.addAction(BTN_TRANSFER_IDX, buttonTransfer, getString(R.string.action_call_attended_transfer), attractorSize);
+ callActions.addAction(BTN_HUNGUP_IDX, buttonHangUp, getString(R.string.action_call_hangup), attractorSize);
+
+ mBubbleModel = new BubbleModel(r.getDisplayMetrics().density, new BubbleModel.ModelCallback() {
+ @Override
+ public void bubbleGrabbed(Bubble b) {
+ if (mBubbleModel.curState != BubbleModel.State.Incall) {
+ return;
+ }
+ if (b.isUser) {
+ mBubbleModel.setActions(b, userActions);
+ } else {
+ mBubbleModel.setActions(b, callActions);
+ }
+ }
+
+ @Override
+ public boolean bubbleEjected(Bubble b) {
+ //if (b.isUser) {
+ try {
+ if (b.isConference())
+ mCallbacks.getService().hangUpConference(b.getCallID());
+ else
+ mCallbacks.getService().hangUp(b.getCallID());
+
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ return true;
+ /*}
+ return false;*/
+ }
+ });
+
+ setHasOptionsMenu(true);
+ PowerManager powerManager = (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
+ mScreenWakeLock = powerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE,
+ "org.sflphone.onIncomingCall");
+ mScreenWakeLock.setReferenceCounted(false);
+
+ Log.d(TAG, "Acquire wake up lock");
+ if (mScreenWakeLock != null && !mScreenWakeLock.isHeld()) {
+ mScreenWakeLock.acquire();
+ }
+
+ mCallbacks.onFragmentCreated();
+ }
+
+ private void initializeWiFiListener() {
+ String connectivity_context = Context.WIFI_SERVICE;
+ wifiManager = (WifiManager) getActivity().getSystemService(connectivity_context);
+ getActivity().registerReceiver(wifiReceiver, new IntentFilter(WifiManager.RSSI_CHANGED_ACTION));
+ }
+
+ /**
+ * A dummy implementation of the {@link Callbacks} interface that does nothing. Used only when this fragment is not attached to an activity.
+ */
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+
+ @Override
+ public void onFragmentCreated() {
+
+ }
+
+ @Override
+ public ISipService getService() {
+ return null;
+ }
+
+ @Override
+ public void terminateCall() {
+ }
+
+ @Override
+ public Conference getDisplayedConference() {
+ return null;
+ }
+
+ @Override
+ public void updateDisplayedConference(Conference c) {
+ }
+
+ @Override
+ public void startTimer() {
+ }
+
+ @Override
+ public void slideChatScreen() {
+ }
+
+ };
+
+ /**
+ * The Activity calling this fragment has to implement this interface
+ */
+ public interface Callbacks {
+
+ public void onFragmentCreated();
+
+ public ISipService getService();
+
+ public void startTimer();
+
+ public void slideChatScreen();
+
+ public void terminateCall();
+
+ public Conference getDisplayedConference();
+
+ public void updateDisplayedConference(Conference c);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ // rootView.requestDisallowInterceptTouchEvent(true);
+
+ mCallbacks = (Callbacks) activity;
+ // myself = SipCall.SipCallBuilder.buildMyselfCall(activity.getContentResolver(), "Me");
+
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu m, MenuInflater inf) {
+ super.onCreateOptionsMenu(m, inf);
+ inf.inflate(R.menu.ac_call, m);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ super.onOptionsItemSelected(item);
+ switch (item.getItemId()) {
+ case R.id.menuitem_chat:
+ mCallbacks.slideChatScreen();
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ initializeWiFiListener();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ getActivity().unregisterReceiver(wifiReceiver);
+ if (mScreenWakeLock != null && mScreenWakeLock.isHeld()) {
+ mScreenWakeLock.release();
+ }
+ }
+
+ @Override
+ public void callStateChanged(Conference updated, String callID, String newState) {
+ mCallbacks.updateDisplayedConference(updated);
+ Log.i(TAG, "Call :" + callID + " " + newState);
+
+ if (getConference().isOnGoing()) {
+ initNormalStateDisplay();
+ } else if (getConference().isRinging()) {
+ mCallStatusTxt.setText(newState);
+
+ if (getConference().isIncoming()) {
+ initIncomingCallDisplay();
+ } else
+ initOutGoingCallDisplay();
+ } else {
+ mCallStatusTxt.setText(newState);
+ mCallbacks.terminateCall();
+ }
+ }
+
+ @Override
+ public void secureZrtpOn(Conference updated, String id) {
+ Log.i(TAG, "secureZrtpOn");
+ mCallbacks.updateDisplayedConference(updated);
+ updateSecurityDisplay();
+ }
+
+ @Override
+ public void secureZrtpOff(Conference updated, String id) {
+ Log.i(TAG, "secureZrtpOff");
+ mCallbacks.updateDisplayedConference(updated);
+ updateSecurityDisplay();
+ }
+
+ @Override
+ public void displaySAS(Conference updated, final String securedCallID) {
+ Log.i(TAG, "displaySAS");
+ mCallbacks.updateDisplayedConference(updated);
+ updateSecurityDisplay();
+ }
+
+ @Override
+ public void zrtpNegotiationFailed(Conference c, String securedCallID) {
+ mCallbacks.updateDisplayedConference(c);
+ updateSecurityDisplay();
+ }
+
+ @Override
+ public void zrtpNotSupported(Conference c, String securedCallID) {
+ mCallbacks.updateDisplayedConference(c);
+ updateSecurityDisplay();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ SipCall transfer;
+ if (requestCode == REQUEST_TRANSFER) {
+ switch (resultCode) {
+ case TransferDFragment.RESULT_TRANSFER_CONF:
+ Conference c = data.getParcelableExtra("target");
+ transfer = data.getParcelableExtra("transfer");
+ try {
+
+ mCallbacks.getService().attendedTransfer(transfer.getCallId(), c.getParticipants().get(0).getCallId());
+
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ break;
+
+ case TransferDFragment.RESULT_TRANSFER_NUMBER:
+ String to = data.getStringExtra("to_number");
+ transfer = data.getParcelableExtra("transfer");
+ try {
+ mCallbacks.getService().transfer(transfer.getCallId(), to);
+ mCallbacks.getService().hangUp(transfer.getCallId());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ break;
+ case Activity.RESULT_CANCELED:
+ default:
+ synchronized (mBubbleModel) {
+ mBubbleModel.clear();
+ }
+ initNormalStateDisplay();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ Log.i(TAG, "onCreateView");
+ final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.frag_call, container, false);
+
+ mBubbleView = (BubblesView) rootView.findViewById(R.id.main_view);
+ //mBubbleView.setFragment(this);
+ mBubbleView.setModel(mBubbleModel);
+ mBubbleView.getHolder().addCallback(this);
+
+ mCallStatusTxt = (TextView) rootView.findViewById(R.id.call_status_txt);
+
+ mSecuritySwitch = (ViewSwitcher) rootView.findViewById(R.id.security_switcher);
+ mToggleSpeakers = (ToggleButton) rootView.findViewById(R.id.speaker_toggle);
+
+ mToggleSpeakers.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ try {
+ mCallbacks.getService().toggleSpeakerPhone(isChecked);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+
+ }
+ });
+
+ synchronized (mBubbleModel) {
+ mBubbleModel.setSize(mBubbleView.getWidth(), mBubbleView.getHeight() - mToggleSpeakers.getHeight(), bubbleSize);
+ }
+
+ rootView.findViewById(R.id.dialpad_btn).setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ InputMethodManager lManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ lManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY);
+ }
+ });
+
+ return rootView;
+ }
+
+ public Conference getConference() {
+ return mCallbacks.getDisplayedConference();
+ }
+
+ private void initNormalStateDisplay() {
+ Log.i(TAG, "Start normal display");
+ synchronized (mBubbleModel) {
+ mCallbacks.startTimer();
+ mBubbleModel.clearAttractors();
+ PointF c = mBubbleModel.getCircleCenter();
+
+ getBubbleForUser(getConference(), (int) c.x, (int) c.y);
+
+ final float angle_part = (float) (2 * Math.PI / getConference().getParticipants().size());
+ final float angle_shift = (float) (Math.PI / 2);
+ float radiusCalls = mBubbleModel.getCircleSize();
+ for (int i = 0; i < getConference().getParticipants().size(); ++i) {
+ SipCall partee = getConference().getParticipants().get(i);
+ if (partee == null) {
+ continue;
+ }
+ float dX = FloatMath.cos(angle_part * i + angle_shift) * radiusCalls;
+ float dY = FloatMath.sin(angle_part * i + angle_shift) * radiusCalls;
+ getBubbleFor(partee, (int) (c.x + dX), (int) (c.y + dY));
+ }
+ }
+ mBubbleModel.curState = BubbleModel.State.Incall;
+ updateSecurityDisplay();
+ }
+
+ private void updateSecurityDisplay() {
+
+ //First we check if at least one participant use a security layer.
+ if (!getConference().useSecureLayer())
+ return;
+
+ Log.i(TAG, "Enable security display");
+ if (getConference().hasMultipleParticipants()) {
+ //TODO What layout should we put?
+ } else {
+ final SecureSipCall secured = (SecureSipCall) getConference().getParticipants().get(0);
+ switch (secured.displayModule()) {
+ case SecureSipCall.DISPLAY_GREEN_LOCK:
+ Log.i(TAG, "DISPLAY_GREEN_LOCK");
+ showLock(R.drawable.green_lock);
+ break;
+ case SecureSipCall.DISPLAY_RED_LOCK:
+ Log.i(TAG, "DISPLAY_RED_LOCK");
+ showLock(R.drawable.red_lock);
+ break;
+ case SecureSipCall.DISPLAY_CONFIRM_SAS:
+ final Button sas = (Button) mSecuritySwitch.findViewById(R.id.confirm_sas);
+ Log.i(TAG, "Confirm SAS: " + secured.getSAS());
+ sas.setText("Confirm SAS: " + secured.getSAS());
+ sas.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ try {
+ mCallbacks.getService().confirmSAS(secured.getCallId());
+ showLock(R.drawable.green_lock);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ mSecuritySwitch.setDisplayedChild(0);
+ mSecuritySwitch.setVisibility(View.VISIBLE);
+ break;
+ case SecureSipCall.DISPLAY_NONE:
+ break;
+ }
+ }
+ }
+
+ private void showLock(int resId) {
+ ImageView lock = (ImageView) mSecuritySwitch.findViewById(R.id.lock_image);
+ lock.setImageDrawable(getResources().getDrawable(resId));
+ mSecuritySwitch.setDisplayedChild(1);
+ mSecuritySwitch.setVisibility(View.VISIBLE);
+ }
+
+ private void initIncomingCallDisplay() {
+ Log.i(TAG, "Start incoming display");
+ if (getConference().getParticipants().get(0).getAccount().isAutoanswerEnabled()) {
+ try {
+ mCallbacks.getService().accept(getConference().getParticipants().get(0).getCallId());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ }
+ } else {
+ getBubbleFor(getConference().getParticipants().get(0), mBubbleModel.getWidth() / 2, 2 * mBubbleModel.getHeight() / 3);
+ synchronized (mBubbleModel) {
+ mBubbleModel.clearAttractors();
+ mBubbleModel.addAttractor(new Attractor(new PointF(3 * mBubbleModel.getWidth() / 4, 2 * mBubbleModel.getHeight() / 3), attractorSize, new Attractor.Callback() {
+ @Override
+ public boolean onBubbleSucked(Bubble b) {
+ if (!accepted) {
+ try {
+ mCallbacks.getService().accept(b.getCallID());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ accepted = true;
+ }
+ return false;
+ }
+ }, buttonCall));
+ mBubbleModel.addAttractor(new Attractor(new PointF(mBubbleModel.getWidth() / 4, 2 * mBubbleModel.getHeight() / 3), attractorSize, new Attractor.Callback() {
+ @Override
+ public boolean onBubbleSucked(Bubble b) {
+ if (!accepted) {
+ try {
+ mCallbacks.getService().refuse(b.getCallID());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ accepted = true;
+ }
+ return false;
+ }
+ }, buttonHangUp));
+ }
+ mBubbleModel.curState = BubbleModel.State.Incoming;
+ }
+ }
+
+ private void initOutGoingCallDisplay() {
+ Log.i(TAG, "Start outgoing display");
+ synchronized (mBubbleModel) {
+ PointF c = mBubbleModel.getCircleCenter();
+ float radiusCalls = mBubbleModel.getCircleSize();
+ getBubbleForUser(getConference(), c.x, c.y);
+ int angle_part = 360 / getConference().getParticipants().size();
+ for (int i = 0; i < getConference().getParticipants().size(); ++i) {
+ double dX = Math.cos(Math.toRadians(angle_part * i + 90)) * radiusCalls;
+ double dY = Math.sin(Math.toRadians(angle_part * i + 90)) * radiusCalls;
+ getBubbleFor(getConference().getParticipants().get(i), (int) (c.x + dX), (int) (c.y + dY));
+ }
+ mBubbleModel.clearAttractors();
+ }
+ mBubbleModel.curState = BubbleModel.State.Outgoing;
+ }
+
+ /**
+ * Retrieves or create a bubble for a given contact. If the bubble exists, it is moved to the new location.
+ *
+ * @param call The call associated to a contact
+ * @param x Initial or new x position.
+ * @param y Initial or new y position.
+ * @return Bubble corresponding to the contact.
+ */
+ private Bubble getBubbleFor(SipCall call, float x, float y) {
+ Bubble contact_bubble = mBubbleModel.getBubble(call.getCallId());
+ if (contact_bubble != null) {
+ ((BubbleContact) contact_bubble).setCall(call);
+ contact_bubble.attractionPoint.set(x, y);
+ return contact_bubble;
+ }
+
+ contact_bubble = new BubbleContact(getActivity(), call, x, y, bubbleSize);
+
+ mBubbleModel.addBubble(contact_bubble);
+ return contact_bubble;
+ }
+
+ private Bubble getBubbleForUser(Conference conf, float x, float y) {
+ Bubble contact_bubble = mBubbleModel.getUser();
+ if (contact_bubble != null) {
+ contact_bubble.attractionPoint.set(x, y);
+ ((BubbleUser) contact_bubble).setConference(conf);
+
+ return contact_bubble;
+ }
+
+ contact_bubble = new BubbleUser(getActivity(), CallContact.ContactBuilder.buildUserContact(getActivity().getContentResolver()), conf, x, y,
+ bubbleSize * 1.3f);
+/*
+ try {
+ ((BubbleUser) contact_bubble).setMute(mCallbacks.getService().isCaptureMuted());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ } catch (NullPointerException e1) {
+ e1.printStackTrace();
+ }*/
+ mBubbleModel.addBubble(contact_bubble);
+ return contact_bubble;
+ }
+
+ public boolean canOpenIMPanel() {
+ return mBubbleModel.curState == BubbleModel.State.Incall && (mBubbleView == null || !mBubbleView.isDraggingBubble());
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ synchronized (mBubbleModel) {
+ mBubbleModel.setSize(width, height, bubbleSize);
+ }
+ if (getConference().getParticipants().size() == 1) {
+ if (getConference().getParticipants().get(0).isIncoming() && getConference().getParticipants().get(0).isRinging()) {
+ initIncomingCallDisplay();
+ } else if (getConference().getParticipants().get(0).isRinging()) {
+ initOutGoingCallDisplay();
+ } else if (getConference().getParticipants().get(0).isOngoing()) {
+ initNormalStateDisplay();
+ }
+ } else if (getConference().getParticipants().size() > 1) {
+ initNormalStateDisplay();
+ }
+ }
+
+ public void makeTransfer(BubbleContact contact) {
+ FragmentManager fm = getFragmentManager();
+ editName = TransferDFragment.newInstance();
+ Bundle b = new Bundle();
+ try {
+ b.putParcelableArrayList("calls", (ArrayList<Conference>) mCallbacks.getService().getConcurrentCalls());
+ b.putParcelable("call_selected", contact.associated_call);
+ editName.setArguments(b);
+ editName.setTargetFragment(this, REQUEST_TRANSFER);
+ editName.show(fm, "");
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ }
+
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ // check that soft input is hidden
+ InputMethodManager lManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ lManager.hideSoftInputFromWindow(mBubbleView.getWindowToken(), 0);
+ if (editName != null && editName.isVisible()) {
+ editName.dismiss();
+ }
+ }
+
+ public BubblesView getBubbleView() {
+ return mBubbleView;
+ }
+
+ public void updateTime() {
+ if (getConference() != null) {
+ long duration = System.currentTimeMillis() - getConference().getParticipants().get(0).getTimestampStart_();
+ duration = duration / 1000;
+ if (getConference().isOnGoing())
+ mCallStatusTxt.setText(String.format("%d:%02d:%02d", duration / 3600, duration % 3600 / 60, duration % 60));
+ }
+
+ }
+
+ public void onKeyUp(int keyCode, KeyEvent event) {
+ try {
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ break;
+ default:
+ String toSend = Character.toString(event.getDisplayLabel());
+ toSend = toSend.toUpperCase(Locale.getDefault());
+ Log.d(TAG, "toSend " + toSend);
+ mCallbacks.getService().playDtmf(toSend);
+ break;
+ }
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/ring-android/src/cx/ring/fragments/CallListFragment.java b/ring-android/src/cx/ring/fragments/CallListFragment.java
new file mode 100644
index 0000000..997085e
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/CallListFragment.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.fragments;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ClipData.Item;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.*;
+import android.util.Log;
+import android.view.DragEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.DragShadowBuilder;
+import android.view.View.OnDragListener;
+import android.view.ViewGroup;
+import android.widget.*;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemLongClickListener;
+
+import cx.ring.client.CallActivity;
+import cx.ring.client.HomeActivity;
+import cx.ring.model.Conference;
+import cx.ring.service.ISipService;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Observable;
+import java.util.Observer;
+
+public class CallListFragment extends CallableWrapperFragment {
+
+ private static final String TAG = CallListFragment.class.getSimpleName();
+
+ private Callbacks mCallbacks = sDummyCallbacks;
+ private TextView mConversationsTitleTextView;
+ CallListAdapter mConferenceAdapter;
+
+ public static final int REQUEST_TRANSFER = 10;
+ public static final int REQUEST_CONF = 20;
+
+ /**
+ * A dummy implementation of the {@link Callbacks} interface that does nothing. Used only when this fragment is not attached to an activity.
+ */
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+
+ @Override
+ public ISipService getService() {
+ Log.i(TAG, "I'm a dummy");
+ return null;
+ }
+
+ };
+
+ @Override
+ public void callStateChanged(Conference c, String callID, String state) {
+ Log.i(TAG, "callStateChanged" + callID + " " + state);
+ updateLists();
+ }
+
+ @Override
+ public void confCreated(Conference c, String id) {
+ Log.i(TAG, "confCreated");
+ updateLists();
+ }
+
+ @Override
+ public void confRemoved(Conference c, String id) {
+ Log.i(TAG, "confRemoved");
+ updateLists();
+ }
+
+ @Override
+ public void confChanged(Conference c, String id, String state) {
+ Log.i(TAG, "confChanged");
+ updateLists();
+ }
+
+ @Override
+ public void recordingChanged(Conference c, String callID, String filename) {
+ Log.i(TAG, "confChanged");
+ updateLists();
+ }
+
+ /**
+ * The Activity calling this fragment has to implement this interface
+ */
+ public interface Callbacks {
+ public ISipService getService();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+
+ }
+
+ private Runnable mUpdateTimeTask = new Runnable() {
+ public void run() {
+ final long start = SystemClock.uptimeMillis();
+ long millis = SystemClock.uptimeMillis() - start;
+ int seconds = (int) (millis / 1000);
+ int minutes = seconds / 60;
+ seconds = seconds % 60;
+
+ mConferenceAdapter.notifyDataSetChanged();
+ mHandler.postAtTime(this, start + (((minutes * 60) + seconds + 1) * 1000));
+ }
+ };
+
+ private Handler mHandler = new Handler();
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mCallbacks.getService() != null) {
+
+ updateLists();
+ if (!mConferenceAdapter.isEmpty()) {
+ mHandler.postDelayed(mUpdateTimeTask, 0);
+ }
+ }
+
+ }
+
+ @SuppressWarnings("unchecked")
+ // No proper solution with HashMap runtime cast
+ public void updateLists() {
+ try {
+ HashMap<String, Conference> confs = (HashMap<String, Conference>) mCallbacks.getService().getConferenceList();
+ String newTitle = getResources().getQuantityString(cx.ring.R.plurals.home_conferences_title, confs.size(), confs.size());
+ mConversationsTitleTextView.setText(newTitle);
+ mConferenceAdapter.updateDataset(new ArrayList<Conference>(confs.values()));
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mHandler.removeCallbacks(mUpdateTimeTask);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ Log.i(TAG, "onCreateView");
+ View inflatedView = inflater.inflate(cx.ring.R.layout.frag_call_list, container, false);
+
+ mConversationsTitleTextView = (TextView) inflatedView.findViewById(cx.ring.R.id.confs_counter);
+
+ mConferenceAdapter = new CallListAdapter(getActivity());
+ ((ListView) inflatedView.findViewById(cx.ring.R.id.confs_list)).setAdapter(mConferenceAdapter);
+ ((ListView) inflatedView.findViewById(cx.ring.R.id.confs_list)).setOnItemClickListener(callClickListener);
+ ((ListView) inflatedView.findViewById(cx.ring.R.id.confs_list)).setOnItemLongClickListener(mItemLongClickListener);
+
+ return inflatedView;
+ }
+
+ OnItemClickListener callClickListener = new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View v, int arg2, long arg3) {
+ Intent intent = new Intent().setClass(getActivity(), CallActivity.class);
+ intent.putExtra("resuming", true);
+ intent.putExtra("conference", (Conference) v.getTag());
+ startActivityForResult(intent, HomeActivity.REQUEST_CODE_CALL);
+ }
+ };
+
+ private OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() {
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> adptv, View view, int pos, long arg3) {
+ final Vibrator vibe = (Vibrator) view.getContext().getSystemService(Context.VIBRATOR_SERVICE);
+ vibe.vibrate(80);
+ Intent i = new Intent();
+ Bundle b = new Bundle();
+ b.putParcelable("conference", (Conference) adptv.getAdapter().getItem(pos));
+ i.putExtra("bconference", b);
+
+ DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);
+ ClipData data = ClipData.newIntent("conference", i);
+ view.startDrag(data, shadowBuilder, view, 0);
+ return false;
+ }
+
+ };
+
+ public class CallListAdapter extends BaseAdapter implements Observer {
+
+ private ArrayList<Conference> calls;
+
+ private Context mContext;
+
+ public CallListAdapter(Context act) {
+ super();
+ mContext = act;
+ calls = new ArrayList<Conference>();
+
+ }
+
+ public void updateDataset(ArrayList<Conference> list) {
+ calls.clear();
+ calls.addAll(list);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return calls.size();
+ }
+
+ @Override
+ public Conference getItem(int position) {
+ return calls.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null)
+ convertView = LayoutInflater.from(mContext).inflate(cx.ring.R.layout.item_calllist, null);
+
+ Conference call = calls.get(position);
+ if (call.getParticipants().size() == 1) {
+ ((TextView) convertView.findViewById(cx.ring.R.id.call_title)).setText(call.getParticipants().get(0).getmContact().getmDisplayName());
+
+ long duration = (System.currentTimeMillis() - (call.getParticipants().get(0).getTimestampStart_())) / 1000;
+
+ ((TextView) convertView.findViewById(cx.ring.R.id.call_time)).setText(String.format("%d:%02d:%02d", duration / 3600, (duration % 3600) / 60,
+ (duration % 60)));
+ } else {
+// String tmp = "Conference with " + call.getParticipants().size() + " participants";
+ ((TextView) convertView.findViewById(cx.ring.R.id.call_title)).setText(getString(cx.ring.R.string.home_conf_item, call.getParticipants().size()));
+ }
+ // ((TextView) convertView.findViewById(R.id.num_participants)).setText("" + call.getParticipants().size());
+ ((TextView) convertView.findViewById(cx.ring.R.id.call_status)).setText(call.getState());
+
+ convertView.setOnDragListener(dragListener);
+ convertView.setTag(call);
+
+ return convertView;
+ }
+
+ @Override
+ public void update(Observable observable, Object data) {
+ Log.i(TAG, "Updating views...");
+ notifyDataSetChanged();
+ }
+
+ }
+
+ OnDragListener dragListener = new OnDragListener() {
+
+ @SuppressWarnings("deprecation")
+ // deprecated in API 16....
+ @Override
+ public boolean onDrag(View v, DragEvent event) {
+ switch (event.getAction()) {
+ case DragEvent.ACTION_DRAG_STARTED:
+ // Do nothing
+ // Log.w(TAG, "ACTION_DRAG_STARTED");
+ break;
+ case DragEvent.ACTION_DRAG_ENTERED:
+ // Log.w(TAG, "ACTION_DRAG_ENTERED");
+ v.setBackgroundColor(Color.GREEN);
+ break;
+ case DragEvent.ACTION_DRAG_EXITED:
+ // Log.w(TAG, "ACTION_DRAG_EXITED");
+ v.setBackgroundDrawable(getResources().getDrawable(cx.ring.R.drawable.item_generic_selector));
+ break;
+ case DragEvent.ACTION_DROP:
+ // Log.w(TAG, "ACTION_DROP");
+ View view = (View) event.getLocalState();
+
+ Item i = event.getClipData().getItemAt(0);
+ Intent intent = i.getIntent();
+ intent.setExtrasClassLoader(Conference.class.getClassLoader());
+
+ Conference initial = (Conference) view.getTag();
+ Conference target = (Conference) v.getTag();
+
+ if (initial == target) {
+ return true;
+ }
+
+ DropActionsChoice dialog = DropActionsChoice.newInstance();
+ Bundle b = new Bundle();
+ b.putParcelable("call_initial", initial);
+ b.putParcelable("call_targeted", target);
+ dialog.setArguments(b);
+ dialog.setTargetFragment(CallListFragment.this, 0);
+ dialog.show(getFragmentManager(), "dialog");
+
+ // view.setBackgroundColor(Color.WHITE);
+ // v.setBackgroundColor(Color.BLACK);
+ break;
+ case DragEvent.ACTION_DRAG_ENDED:
+ // Log.w(TAG, "ACTION_DRAG_ENDED");
+ View view1 = (View) event.getLocalState();
+ view1.setVisibility(View.VISIBLE);
+ v.setBackgroundDrawable(getResources().getDrawable(cx.ring.R.drawable.item_generic_selector));
+ default:
+ break;
+ }
+ return true;
+ }
+
+ };
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ Conference transfer;
+ if (requestCode == REQUEST_TRANSFER) {
+ switch (resultCode) {
+ case 0:
+ Conference c = data.getParcelableExtra("target");
+ transfer = data.getParcelableExtra("transfer");
+ try {
+ mCallbacks.getService().attendedTransfer(transfer.getParticipants().get(0).getCallId(), c.getParticipants().get(0).getCallId());
+ mConferenceAdapter.notifyDataSetChanged();
+ } catch (RemoteException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ Toast.makeText(getActivity(), getString(cx.ring.R.string.home_transfer_complet), Toast.LENGTH_LONG).show();
+ break;
+
+ case 1:
+ String to = data.getStringExtra("to_number");
+ transfer = data.getParcelableExtra("transfer");
+ try {
+ Toast.makeText(getActivity(), getString(cx.ring.R.string.home_transfering, transfer.getParticipants().get(0).getmContact().getmDisplayName(), to),
+ Toast.LENGTH_SHORT).show();
+ mCallbacks.getService().transfer(transfer.getParticipants().get(0).getCallId(), to);
+ mCallbacks.getService().hangUp(transfer.getParticipants().get(0).getCallId());
+ } catch (RemoteException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ break;
+
+ default:
+ break;
+ }
+ } else if (requestCode == REQUEST_CONF) {
+ switch (resultCode) {
+ case 0:
+ Conference call_to_add = data.getParcelableExtra("transfer");
+ Conference call_target = data.getParcelableExtra("target");
+
+ bindCalls(call_to_add, call_target);
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ private void bindCalls(Conference call_to_add, Conference call_target) {
+ try {
+
+ Log.i(TAG, "joining calls:" + call_to_add.getId() + " and " + call_target.getId());
+
+ if (call_target.hasMultipleParticipants() && !call_to_add.hasMultipleParticipants()) {
+
+ mCallbacks.getService().addParticipant(call_to_add.getParticipants().get(0), call_target.getId());
+
+ } else if (call_target.hasMultipleParticipants() && call_to_add.hasMultipleParticipants()) {
+
+ // We join two conferences
+ mCallbacks.getService().joinConference(call_to_add.getId(), call_target.getId());
+
+ } else if (!call_target.hasMultipleParticipants() && call_to_add.hasMultipleParticipants()) {
+
+ mCallbacks.getService().addParticipant(call_target.getParticipants().get(0), call_to_add.getId());
+
+ } else {
+ // We join two single calls to create a conf
+ mCallbacks.getService().joinParticipant(call_to_add.getParticipants().get(0).getCallId(),
+ call_target.getParticipants().get(0).getCallId());
+ }
+
+ } catch (RemoteException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/ring-android/src/cx/ring/fragments/CallableWrapperFragment.java b/ring-android/src/cx/ring/fragments/CallableWrapperFragment.java
new file mode 100644
index 0000000..53ccc01
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/CallableWrapperFragment.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.fragments;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import cx.ring.interfaces.CallInterface;
+import cx.ring.model.Conference;
+import cx.ring.service.CallManagerCallBack;
+
+import java.util.HashMap;
+
+public abstract class CallableWrapperFragment extends Fragment implements CallInterface {
+
+
+ private CallReceiver mReceiver;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mReceiver = new CallReceiver();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(CallManagerCallBack.INCOMING_CALL);
+ intentFilter.addAction(CallManagerCallBack.INCOMING_TEXT);
+ intentFilter.addAction(CallManagerCallBack.CALL_STATE_CHANGED);
+ intentFilter.addAction(CallManagerCallBack.CONF_CREATED);
+ intentFilter.addAction(CallManagerCallBack.CONF_REMOVED);
+ intentFilter.addAction(CallManagerCallBack.CONF_CHANGED);
+ intentFilter.addAction(CallManagerCallBack.RECORD_STATE_CHANGED);
+ intentFilter.addAction(CallManagerCallBack.ZRTP_OFF);
+ intentFilter.addAction(CallManagerCallBack.ZRTP_ON);
+ intentFilter.addAction(CallManagerCallBack.DISPLAY_SAS);
+ intentFilter.addAction(CallManagerCallBack.ZRTP_NEGOTIATION_FAILED);
+ intentFilter.addAction(CallManagerCallBack.ZRTP_NOT_SUPPORTED);
+ intentFilter.addAction(CallManagerCallBack.RTCP_REPORT_RECEIVED);
+ getActivity().registerReceiver(mReceiver, intentFilter);
+ }
+
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ getActivity().unregisterReceiver(mReceiver);
+ }
+
+ @Override
+ public void callStateChanged(Conference c, String callID, String state) {
+
+ }
+
+ @Override
+ public void incomingText(Conference c, String ID, String from, String msg) {
+
+ }
+
+ @Override
+ public void confCreated(Conference c, String id) {
+
+ }
+
+ @Override
+ public void confRemoved(Conference c, String id) {
+
+ }
+
+ @Override
+ public void confChanged(Conference c, String id, String state) {
+
+ }
+
+ @Override
+ public void recordingChanged(Conference c, String callID, String filename) {
+
+ }
+
+ @Override
+ public void secureZrtpOn(Conference c, String id) {
+
+ }
+
+ @Override
+ public void secureZrtpOff(Conference c, String id) {
+
+ }
+
+ @Override
+ public void displaySAS(Conference c, String securedCallID) {
+
+ }
+
+ @Override
+ public void zrtpNegotiationFailed(Conference c, String securedCallID) {
+
+ }
+
+ @Override
+ public void zrtpNotSupported(Conference c, String securedCallID) {
+
+ }
+
+ @Override
+ public void rtcpReportReceived(Conference c, HashMap<String, Integer> stats) {
+
+ }
+
+
+ public class CallReceiver extends BroadcastReceiver {
+
+ private final String TAG = CallReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().contentEquals(CallManagerCallBack.INCOMING_TEXT)) {
+ incomingText((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("CallID"), intent.getStringExtra("From"), intent.getStringExtra("Msg"));
+ } else if (intent.getAction().contentEquals(CallManagerCallBack.CALL_STATE_CHANGED)) {
+ callStateChanged((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("CallID"), intent.getStringExtra("State"));
+ } else if (intent.getAction().contentEquals(CallManagerCallBack.CONF_CREATED)) {
+ confCreated((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("confID"));
+ } else if (intent.getAction().contentEquals(CallManagerCallBack.CONF_REMOVED)) {
+ confRemoved((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("confID"));
+ } else if (intent.getAction().contentEquals(CallManagerCallBack.CONF_CHANGED)) {
+ confChanged((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("confID"), intent.getStringExtra("state"));
+ } else if (intent.getAction().contentEquals(CallManagerCallBack.RECORD_STATE_CHANGED)) {
+ recordingChanged((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("callID"), intent.getStringExtra("file"));
+ } else if (intent.getAction().contentEquals(CallManagerCallBack.ZRTP_OFF)) {
+ secureZrtpOff((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("callID"));
+ } else if (intent.getAction().contentEquals(CallManagerCallBack.ZRTP_ON)) {
+ secureZrtpOn((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("callID"));
+ } else if (intent.getAction().contentEquals(CallManagerCallBack.DISPLAY_SAS)) {
+ displaySAS((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("callID"));
+ } else if (intent.getAction().contentEquals(CallManagerCallBack.ZRTP_NEGOTIATION_FAILED)) {
+ zrtpNegotiationFailed((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("callID"));
+ } else if (intent.getAction().contentEquals(CallManagerCallBack.ZRTP_NOT_SUPPORTED)) {
+ zrtpNotSupported((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("callID"));
+ } else if (intent.getAction().contentEquals(CallManagerCallBack.RTCP_REPORT_RECEIVED)) {
+ rtcpReportReceived(null, null); // FIXME
+ } else {
+ Log.e(TAG, "Unknown action: " + intent.getAction());
+ }
+
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/fragments/ConferenceDFragment.java b/ring-android/src/cx/ring/fragments/ConferenceDFragment.java
new file mode 100644
index 0000000..7bfbafa
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/ConferenceDFragment.java
@@ -0,0 +1,163 @@
+package cx.ring.fragments;
+
+import java.util.ArrayList;
+
+import cx.ring.R;
+import cx.ring.loaders.ContactsLoader;
+import cx.ring.model.Conference;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class ConferenceDFragment extends DialogFragment implements LoaderManager.LoaderCallbacks<Bundle> {
+
+
+ SimpleCallListAdapter mAdapter;
+
+ /**
+ * Create a new instance of CallActionsDFragment
+ */
+ public static ConferenceDFragment newInstance() {
+ ConferenceDFragment f = new ConferenceDFragment();
+ return f;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Pick a style based on the num.
+ int style = DialogFragment.STYLE_NORMAL, theme = 0;
+ setStyle(style, theme);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ View rootView = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_conference, null);
+
+ ArrayList<Conference> calls = getArguments().getParcelableArrayList("calls");
+ final Conference call_selected = getArguments().getParcelable("call_selected");
+
+ mAdapter = new SimpleCallListAdapter(getActivity(), calls);
+ ListView list = (ListView) rootView.findViewById(R.id.concurrent_calls);
+ list.setAdapter(mAdapter);
+ list.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1, int pos, long arg3) {
+
+ Intent in = new Intent();
+
+ in.putExtra("transfer", call_selected);
+ in.putExtra("target", mAdapter.getItem(pos));
+ getTargetFragment().onActivityResult(getTargetRequestCode(), 0, in);
+ dismiss();
+ }
+ });
+ list.setEmptyView(rootView.findViewById(R.id.empty_view));
+
+
+
+ final AlertDialog a = new AlertDialog.Builder(getActivity()).setView(rootView).setTitle("Transfer " + call_selected.getParticipants().get(0).getmContact())
+ .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+
+ dismiss();
+ }
+ }).create();
+
+ return a;
+ }
+
+ @Override
+ public Loader<Bundle> onCreateLoader(int id, Bundle args) {
+ Uri baseUri;
+
+ if (args != null) {
+ baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(args.getString("filter")));
+ } else {
+ baseUri = Contacts.CONTENT_URI;
+ }
+ ContactsLoader l = new ContactsLoader(getActivity(), baseUri);
+ l.forceLoad();
+ return l;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Bundle> loader, Bundle data) {
+
+// ArrayList<CallContact> tmp = data.getParcelableArrayList("Contacts");
+
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Bundle> loader) {
+ // Thi is called when the last Cursor provided to onLoadFinished
+ // mListAdapter.swapCursor(null);
+ }
+
+
+
+ private class SimpleCallListAdapter extends BaseAdapter {
+
+ private LayoutInflater mInflater;
+ ArrayList<Conference> calls;
+
+ public SimpleCallListAdapter(final Context context, ArrayList<Conference> calls2) {
+ super();
+ mInflater = LayoutInflater.from(context);
+ calls = calls2;
+ }
+
+ @Override
+ public View getView(final int position, final View convertView, final ViewGroup parent) {
+ final TextView tv;
+ if (convertView != null) {
+ tv = (TextView) convertView;
+ } else {
+ tv = (TextView) mInflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);
+ }
+
+ if(calls.get(position).getParticipants().size() == 1){
+ tv.setText(calls.get(position).getParticipants().get(0).getmContact().getmDisplayName());
+ } else {
+ tv.setText("Conference with "+ calls.get(position).getParticipants().size() + " participants");
+ }
+
+ return tv;
+ }
+
+ @Override
+ public int getCount() {
+ return calls.size();
+ }
+
+ @Override
+ public Conference getItem(int pos) {
+ return calls.get(pos);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+ }
+
+}
diff --git a/ring-android/src/cx/ring/fragments/ContactListFragment.java b/ring-android/src/cx/ring/fragments/ContactListFragment.java
new file mode 100644
index 0000000..9640452
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/ContactListFragment.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
+ * Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.fragments;
+
+import java.util.ArrayList;
+
+import cx.ring.R;
+import cx.ring.adapters.ContactsAdapter;
+import cx.ring.adapters.StarredContactsAdapter;
+import cx.ring.loaders.ContactsLoader;
+import cx.ring.loaders.LoaderConstants;
+import cx.ring.model.CallContact;
+import cx.ring.service.ISipService;
+import cx.ring.views.SwipeListViewTouchListener;
+import cx.ring.views.stickylistheaders.StickyListHeadersListView;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.LoaderManager;
+import android.content.Loader;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+import android.util.Log;
+import android.view.DragEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.DragShadowBuilder;
+import android.view.View.MeasureSpec;
+import android.view.View.OnClickListener;
+import android.view.View.OnDragListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.GridView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+import android.widget.SearchView;
+import android.widget.SearchView.OnQueryTextListener;
+
+public class ContactListFragment extends Fragment implements OnQueryTextListener, LoaderManager.LoaderCallbacks<Bundle> {
+ private static final String TAG = "ContactListFragment";
+ ContactsAdapter mListAdapter;
+ StarredContactsAdapter mGridAdapter;
+ SearchView mQuickReturnSearchView;
+ String mCurFilter;
+ StickyListHeadersListView mContactList;
+ private GridView mStarredGrid;
+ private SwipeListViewTouchListener mSwipeLvTouchListener;
+ private LinearLayout mHeader;
+
+ @Override
+ public void onCreate(Bundle savedInBundle) {
+ super.onCreate(savedInBundle);
+ mGridAdapter = new StarredContactsAdapter(getActivity());
+ mListAdapter = new ContactsAdapter(this);
+ }
+
+ public Callbacks mCallbacks = sDummyCallbacks;
+ private LinearLayout llMain;
+ /**
+ * A dummy implementation of the {@link Callbacks} interface that does nothing. Used only when this fragment is not attached to an activity.
+ */
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+ @Override
+ public void onCallContact(CallContact c) {
+ }
+
+ @Override
+ public void onTextContact(CallContact c) {
+ }
+
+ @Override
+ public void onEditContact(CallContact c) {
+ }
+
+ @Override
+ public ISipService getService() {
+ Log.i(TAG, "Dummy");
+ return null;
+ }
+
+ @Override
+ public void onContactDragged() {
+ }
+
+ @Override
+ public void toggleDrawer() {
+ }
+
+ @Override
+ public void setDragView(RelativeLayout relativeLayout) {
+
+ }
+
+ @Override
+ public void toggleForSearchDrawer() {
+ }
+ };
+
+ public interface Callbacks {
+ void onCallContact(CallContact c);
+
+ void onTextContact(CallContact c);
+
+ public ISipService getService();
+
+ void onContactDragged();
+
+ void toggleDrawer();
+
+ void onEditContact(CallContact item);
+
+ void setDragView(RelativeLayout relativeLayout);
+
+ void toggleForSearchDrawer();
+
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View inflatedView = inflater.inflate(R.layout.frag_contact_list, container, false);
+ mHeader = (LinearLayout) inflater.inflate(R.layout.frag_contact_list_header, null);
+ mContactList = (StickyListHeadersListView) inflatedView.findViewById(R.id.contacts_stickylv);
+
+ inflatedView.findViewById(R.id.drag_view).setOnTouchListener(new OnTouchListener() {
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ return true;
+ }
+ });
+
+ inflatedView.findViewById(R.id.contact_search_button).setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ mContactList.smoothScrollToPosition(0);
+ mQuickReturnSearchView.setOnQueryTextListener(ContactListFragment.this);
+ mQuickReturnSearchView.setIconified(false);
+ mQuickReturnSearchView.setFocusable(true);
+ mCallbacks.toggleForSearchDrawer();
+ }
+ });
+
+ inflatedView.findViewById(R.id.slider_button).setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ mCallbacks.toggleDrawer();
+ }
+ });
+
+ mCallbacks.setDragView(((RelativeLayout) inflatedView.findViewById(R.id.slider_button)));
+
+ mQuickReturnSearchView = (SearchView) mHeader.findViewById(R.id.contact_search);
+ mStarredGrid = (GridView) mHeader.findViewById(R.id.favorites_grid);
+ llMain = (LinearLayout) mHeader.findViewById(R.id.llMain);
+ return inflatedView;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mContactList.addHeaderView(mHeader, null, false);
+ mContactList.setAdapter(mListAdapter);
+
+ mStarredGrid.setAdapter(mGridAdapter);
+ mQuickReturnSearchView.setIconifiedByDefault(false);
+
+ mQuickReturnSearchView.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ mQuickReturnSearchView.setIconified(false);
+ mQuickReturnSearchView.setFocusable(true);
+ }
+ });
+ mQuickReturnSearchView.setOnQueryTextListener(ContactListFragment.this);
+
+ getLoaderManager().initLoader(LoaderConstants.CONTACT_LOADER, null, this);
+
+ }
+
+ private OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView<?> av, View view, int pos, long id) {
+ DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view.findViewById(R.id.photo));
+ view.startDrag(null, shadowBuilder, view, 0);
+ mCallbacks.onContactDragged();
+ return true;
+ }
+
+ };
+
+ private void setGridViewListeners() {
+ mStarredGrid.setOnDragListener(dragListener);
+ mStarredGrid.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View v, int pos, long arg3) {
+ mCallbacks.onCallContact(mGridAdapter.getItem(pos));
+ }
+ });
+ mStarredGrid.setOnItemLongClickListener(mItemLongClickListener);
+ }
+
+ private void setListViewListeners() {
+ mSwipeLvTouchListener = new SwipeListViewTouchListener(mContactList.getWrappedList(), new SwipeListViewTouchListener.OnSwipeCallback() {
+ @Override
+ public void onSwipeLeft(ListView listView, int[] reverseSortedPositions) {
+ }
+
+ @Override
+ public void onSwipeRight(ListView listView, View down) {
+ down.findViewById(R.id.quick_edit).setClickable(true);
+ down.findViewById(R.id.quick_discard).setClickable(true);
+ down.findViewById(R.id.quick_starred).setClickable(true);
+
+ }
+ }, true, false);
+
+ mContactList.getWrappedList().setOnDragListener(dragListener);
+ mContactList.getWrappedList().setOnTouchListener(mSwipeLvTouchListener);
+ mContactList.getWrappedList().setOnItemLongClickListener(mItemLongClickListener);
+ mContactList.getWrappedList().setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View view, int pos, long id) {
+ Log.i(TAG, "Opening Item");
+ mSwipeLvTouchListener.openItem(view, pos, id);
+ }
+ });
+ }
+
+ OnDragListener dragListener = new OnDragListener() {
+
+ @Override
+ public boolean onDrag(View v, DragEvent event) {
+ switch (event.getAction()) {
+ case DragEvent.ACTION_DRAG_STARTED:
+ // Do nothing
+ break;
+ case DragEvent.ACTION_DRAG_ENTERED:
+ break;
+ case DragEvent.ACTION_DRAG_EXITED:
+ // v.setBackgroundDrawable(null);
+ break;
+ case DragEvent.ACTION_DROP:
+ break;
+ case DragEvent.ACTION_DRAG_ENDED:
+ View view1 = (View) event.getLocalState();
+ view1.setVisibility(View.VISIBLE);
+ default:
+ break;
+ }
+ return true;
+ }
+
+ };
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ if (newText.isEmpty()) {
+ getLoaderManager().restartLoader(LoaderConstants.CONTACT_LOADER, null, this);
+ return true;
+ }
+ mCurFilter = newText;
+ Bundle b = new Bundle();
+ b.putString("filter", mCurFilter);
+ getLoaderManager().restartLoader(LoaderConstants.CONTACT_LOADER, b, this);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ // Return false to let the SearchView perform the default action
+ return false;
+ }
+
+ @Override
+ public Loader<Bundle> onCreateLoader(int id, Bundle args) {
+ Uri baseUri;
+
+ Log.i(TAG, "createLoader");
+
+ if (args != null) {
+ baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(args.getString("filter")));
+ } else {
+ baseUri = Contacts.CONTENT_URI;
+ }
+ ContactsLoader l = new ContactsLoader(getActivity(), baseUri);
+ l.forceLoad();
+ return l;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Bundle> loader, Bundle data) {
+
+ mGridAdapter.removeAll();
+ mListAdapter.clear();
+ ArrayList<CallContact> tmp = data.getParcelableArrayList("Contacts");
+ ArrayList<CallContact> tmp2 = data.getParcelableArrayList("Starred");
+ mListAdapter.addAll(tmp);
+ mGridAdapter.addAll(tmp2);
+
+ setListViewListeners();
+ setGridViewListeners();
+
+ mStarredGrid.post(new Runnable() {
+
+ @Override
+ public void run() {
+ setGridViewHeight(mStarredGrid, llMain);
+ }
+ });
+
+ }
+
+ // Sets the GridView holder's height to fully expand it
+ public void setGridViewHeight(GridView gridView, LinearLayout llMain) {
+ ListAdapter listAdapter = gridView.getAdapter();
+ if (listAdapter == null) {
+ return;
+ }
+
+ int totalHeight = 0;
+ int firstHeight = 0;
+ int desiredWidth = MeasureSpec.makeMeasureSpec(gridView.getWidth(), MeasureSpec.AT_MOST);
+
+ int rows = (listAdapter.getCount() + gridView.getNumColumns() - 1) / gridView.getNumColumns();
+
+ for (int i = 0; i < rows; i++) {
+ if (i == 0) {
+ View listItem = listAdapter.getView(i, null, gridView);
+ listItem.measure(desiredWidth, MeasureSpec.UNSPECIFIED);
+ firstHeight = listItem.getMeasuredHeight();
+ }
+ totalHeight += firstHeight;
+ }
+
+ LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) llMain.getLayoutParams();
+
+ params.height = (int) (totalHeight + (getResources().getDimension(R.dimen.contact_vertical_spacing) * (rows - 1) + llMain.getPaddingBottom() + llMain.getPaddingTop()));
+ llMain.setLayoutParams(params);
+ mHeader.requestLayout();
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Bundle> loader) {
+ }
+}
diff --git a/ring-android/src/cx/ring/fragments/DetailsHistoryEntryFragment.java b/ring-android/src/cx/ring/fragments/DetailsHistoryEntryFragment.java
new file mode 100644
index 0000000..0ca2b98
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/DetailsHistoryEntryFragment.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.fragments;
+
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.*;
+import cx.ring.R;
+import cx.ring.adapters.ContactPictureTask;
+import cx.ring.history.HistoryCall;
+import cx.ring.history.HistoryEntry;
+import cx.ring.model.account.Account;
+import cx.ring.model.SipCall;
+import cx.ring.service.ISipService;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.NavigableMap;
+import java.util.Random;
+
+public class DetailsHistoryEntryFragment extends Fragment {
+
+ DetailHistoryAdapter mAdapter;
+ HistoryEntry toDisplay;
+ @SuppressWarnings("unused")
+ private static final String TAG = DetailsHistoryEntryFragment.class.getSimpleName();
+ ContactPictureTask tasker;
+
+ private ListView lvMain;
+ private LinearLayout llMain;
+ private RelativeLayout iv;
+
+ private Callbacks mCallbacks = sDummyCallbacks;
+
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+
+ @Override
+ public ISipService getService() {
+ return null;
+ }
+
+ @Override
+ public void onCall(SipCall call) {
+ }
+
+ };
+
+ public interface Callbacks {
+
+ public ISipService getService();
+
+ public void onCall(SipCall call);
+
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ toDisplay = (HistoryEntry) getArguments().get("entry");
+ mAdapter = new DetailHistoryAdapter(toDisplay.getCalls(), getActivity());
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
+ View inflatedView = inflater.inflate(R.layout.frag_history_detail, parent, false);
+
+ llMain = (LinearLayout) inflatedView.findViewById(R.id.llMain);
+ /*llMainHolder = (LinearLayout) inflatedView.findViewById(R.id.llMainHolder);*/
+ lvMain = (ListView) inflatedView.findViewById(R.id.lvMain);
+ lvMain.setAdapter(mAdapter);
+ iv = (RelativeLayout) inflatedView.findViewById(R.id.iv);
+
+ ((TextView) iv.findViewById(R.id.history_call_name)).setText(toDisplay.getContact().getmDisplayName());
+
+ tasker = new ContactPictureTask(getActivity(), (ImageView) inflatedView.findViewById(R.id.contact_photo), toDisplay.getContact());
+ tasker.run();
+// ((TextView) iv.findViewById(R.id.history_entry_number)).setText(getString(R.string.detail_hist_call_number, toDisplay.getNumber()));
+ iv.findViewById(R.id.history_call_name).setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ try {
+ HashMap<String, String> details = (HashMap<String, String>) mCallbacks.getService().getAccountDetails(toDisplay.getAccountID());
+ ArrayList<HashMap<String, String>> creds = (ArrayList<HashMap<String, String>>) mCallbacks.getService().getCredentials(toDisplay.getAccountID());
+ Bundle args = new Bundle();
+ args.putString(SipCall.ID, Integer.toString(Math.abs(new Random().nextInt())));
+ args.putParcelable(SipCall.ACCOUNT, new Account(toDisplay.getAccountID(), details, creds));
+ args.putInt(SipCall.STATE, SipCall.state.CALL_STATE_RINGING);
+ args.putInt(SipCall.TYPE, SipCall.direction.CALL_TYPE_OUTGOING);
+ args.putParcelable(SipCall.CONTACT, toDisplay.getContact());
+
+ mCallbacks.onCall(new SipCall(args));
+
+ } catch (RemoteException e) {
+ // TODO Bloc catch généré automatiquement
+ e.printStackTrace();
+ }
+ }
+ });
+ return inflatedView;
+ }
+
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ }
+
+ private class DetailHistoryAdapter extends BaseAdapter implements ListAdapter {
+
+ ArrayList<HistoryCall> dataset;
+ Context mContext;
+
+ public DetailHistoryAdapter(NavigableMap<Long, HistoryCall> calls, Context c) {
+ dataset = new ArrayList<HistoryCall>(calls.descendingMap().values());
+ mContext = c;
+ }
+
+ @Override
+ public int getCount() {
+ return dataset.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return dataset.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ HistoryCallView entryView = null;
+
+ if (convertView == null) {
+ // Get a new instance of the row layout view
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ convertView = inflater.inflate(R.layout.item_history_call, null);
+
+ // Hold the view objects in an object
+ // so they don't need to be re-fetched
+ entryView = new HistoryCallView();
+ entryView.historyCallState = (TextView) convertView.findViewById(R.id.history_call_state);
+ entryView.formatted_date = (TextView) convertView.findViewById(R.id.history_call_date_formatted);
+ entryView.formatted_hour = (TextView) convertView.findViewById(R.id.history_call_hour);
+ entryView.record = (Button) convertView.findViewById(R.id.history_call_record);
+ entryView.duration = (TextView) convertView.findViewById(R.id.history_call_duration);
+
+ convertView.setTag(entryView);
+ } else {
+ entryView = (HistoryCallView) convertView.getTag();
+ }
+
+ final HistoryCall item = dataset.get(position);
+
+ entryView.historyCallState.setText(item.getDirection());
+ entryView.formatted_date.setText(item.getDate());
+ entryView.duration.setText(item.getDurationString());
+ entryView.formatted_hour.setText(item.getStartString("h:mm a"));
+ if (item.isIncoming() && item.isMissed())
+ convertView.setBackgroundColor(getResources().getColor(R.color.holo_red_light));
+
+ if (item.hasRecord()) {
+ entryView.record.setVisibility(View.VISIBLE);
+ entryView.record.setTag(R.id.history_call_record, true);
+ entryView.record.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ try {
+ if ((Boolean) v.getTag(R.id.history_call_record)) {
+ mCallbacks.getService().startRecordedFilePlayback(item.getRecordPath());
+ v.setTag(R.id.replay, false);
+ ((Button) v).setText(getString(R.string.hist_replay_button_stop));
+ } else {
+ mCallbacks.getService().stopRecordedFilePlayback(item.getRecordPath());
+ v.setTag(R.id.history_call_record, true);
+ ((Button) v).setText(getString(R.string.hist_replay_button));
+ }
+ } catch (RemoteException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ return convertView;
+ }
+
+ /**
+ * ******************
+ * ViewHolder Pattern
+ * *******************
+ */
+ public class HistoryCallView {
+ protected TextView historyCallState;
+ protected TextView formatted_date;
+ protected TextView formatted_hour;
+ protected Button record;
+ protected TextView duration;
+ }
+
+ }
+
+}
diff --git a/ring-android/src/cx/ring/fragments/DialingFragment.java b/ring-android/src/cx/ring/fragments/DialingFragment.java
new file mode 100644
index 0000000..7a634f9
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/DialingFragment.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.fragments;
+
+import java.util.Locale;
+
+import android.support.v4.app.Fragment;
+import cx.ring.R;
+import cx.ring.service.ISipService;
+import cx.ring.views.ClearableEditText;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+public class DialingFragment extends Fragment implements OnTouchListener {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = DialingFragment.class.getSimpleName();
+
+ ClearableEditText textField;
+ private Callbacks mCallbacks = sDummyCallbacks;
+
+ /**
+ * A dummy implementation of the {@link Callbacks} interface that does nothing. Used only when this fragment is not attached to an activity.
+ */
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+ @Override
+ public void onCallDialed(String to) {
+ }
+
+ @Override
+ public ISipService getService() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+ };
+
+ /**
+ * The Activity calling this fragment has to implement this interface
+ *
+ */
+ public interface Callbacks {
+ void onCallDialed(String account);
+
+ public ISipService getService();
+
+ }
+
+ @Override
+ public void setUserVisibleHint(boolean isVisibleToUser) {
+ super.setUserVisibleHint(isVisibleToUser);
+ if (!isVisibleToUser && isAdded()) {
+ InputMethodManager lManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ textField.setError(null);
+ textField.getEdit_text().setText("");
+ lManager.hideSoftInputFromWindow(textField.getWindowToken(), 0);
+ }
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
+ View inflatedView = inflater.inflate(R.layout.frag_dialing, parent, false);
+
+ textField = (ClearableEditText) inflatedView.findViewById(R.id.textField);
+ inflatedView.findViewById(R.id.buttonCall).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ String to = textField.getText().toString();
+ if (to.contentEquals("")) {
+ textField.setError(getString(R.string.dial_error_no_number_dialed));
+ } else {
+ mCallbacks.onCallDialed(to);
+ }
+ }
+ });
+
+ inflatedView.setOnTouchListener(this);
+
+ inflatedView.findViewById(R.id.alphabetic_keyboard).setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ textField.setInputType(EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+ InputMethodManager lManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ lManager.showSoftInput(textField.getEdit_text(), 0);
+ }
+ });
+
+ inflatedView.findViewById(R.id.numeric_keyboard).setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ textField.setInputType(EditorInfo.TYPE_CLASS_PHONE);
+ InputMethodManager lManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ lManager.showSoftInput(textField.getEdit_text(), 0);
+ }
+ });
+
+ textField.setOnEditorActionListener(new OnEditorActionListener() {
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ String to = textField.getText().toString();
+ if (to.contentEquals("")) {
+ textField.setError(getString(R.string.dial_error_no_number_dialed));
+ } else {
+ mCallbacks.onCallDialed(to);
+ }
+ return true;
+ }
+ });
+ return inflatedView;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ textField.getEdit_text().setText("");
+ textField.setTextWatcher(dtmfKeyListener);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ textField.unsetTextWatcher();
+ }
+
+ TextWatcher dtmfKeyListener = new TextWatcher() {
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (count - before > 1 || count == 0)
+ return; // pasted a number (not implemented yet)
+
+ try {
+ String toSend = Character.toString(s.charAt(start));
+ toSend.toUpperCase(Locale.getDefault());
+ mCallbacks.getService().playDtmf(toSend);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ InputMethodManager lManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ textField.setError(null);
+ lManager.hideSoftInputFromWindow(textField.getWindowToken(), 0);
+ return false;
+ }
+
+}
diff --git a/ring-android/src/cx/ring/fragments/DropActionsChoice.java b/ring-android/src/cx/ring/fragments/DropActionsChoice.java
new file mode 100644
index 0000000..5e09f23
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/DropActionsChoice.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.fragments;
+
+import android.support.v4.app.DialogFragment;
+import cx.ring.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+public class DropActionsChoice extends DialogFragment {
+
+ ListAdapter mAdapter;
+ private Bundle args;
+ public static final int REQUEST_TRANSFER = 10;
+ public static final int REQUEST_CONF = 20;
+
+ /**
+ * Create a new instance of CallActionsDFragment
+ */
+ public static DropActionsChoice newInstance() {
+ DropActionsChoice f = new DropActionsChoice();
+ return f;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Pick a style based on the num.
+ int style = DialogFragment.STYLE_NORMAL, theme = 0;
+ setStyle(style, theme);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ ListView rootView = new ListView(getActivity());
+
+ args = getArguments();
+ mAdapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, getResources().getStringArray(R.array.drop_actions));
+
+ // ListView list = (ListView) rootView.findViewById(R.id.concurrent_calls);
+ rootView.setAdapter(mAdapter);
+ rootView.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1, int pos, long arg3) {
+ Intent in = new Intent();
+
+ in.putExtra("transfer", args.getParcelable("call_initial"));
+ in.putExtra("target", args.getParcelable("call_targeted"));
+
+ switch (pos) {
+ case 0: // Transfer
+ getTargetFragment().onActivityResult(REQUEST_TRANSFER, 0, in);
+ break;
+ case 1: // Conference
+ getTargetFragment().onActivityResult(REQUEST_CONF, 0, in);
+ break;
+ }
+ dismiss();
+
+ }
+ });
+
+ final AlertDialog a = new AlertDialog.Builder(getActivity()).setView(rootView).setTitle("Choose Action")
+ .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ dismiss();
+ }
+ }).create();
+
+ return a;
+ }
+
+}
diff --git a/ring-android/src/cx/ring/fragments/GeneralAccountFragment.java b/ring-android/src/cx/ring/fragments/GeneralAccountFragment.java
new file mode 100644
index 0000000..6e9a5f2
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/GeneralAccountFragment.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.fragments;
+
+import cx.ring.R;
+import cx.ring.model.account.AccountDetail;
+import cx.ring.model.account.AccountDetailBasic;
+import cx.ring.model.account.Account;
+import cx.ring.views.PasswordPreference;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.PreferenceFragment;
+import android.util.Log;
+
+public class GeneralAccountFragment extends PreferenceFragment {
+
+ private static final String TAG = GeneralAccountFragment.class.getSimpleName();
+ private Callbacks mCallbacks = sDummyCallbacks;
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+
+ @Override
+ public Account getAccount() {
+ return null;
+ }
+
+ };
+
+ public interface Callbacks {
+
+ public Account getAccount();
+
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.account_general_prefs);
+ setPreferenceDetails(mCallbacks.getAccount().getBasicDetails());
+ addPreferenceListener(mCallbacks.getAccount().getBasicDetails(), changeBasicPreferenceListener);
+
+ }
+
+ private void setPreferenceDetails(AccountDetail details) {
+ for (AccountDetail.PreferenceEntry p : details.getDetailValues()) {
+ Log.i(TAG, "setPreferenceDetails: pref " + p.mKey + " value " + p.mValue);
+ Preference pref = findPreference(p.mKey);
+ if (pref != null) {
+ if (!p.isTwoState) {
+ ((EditTextPreference) pref).setText(p.mValue);
+ if (pref instanceof PasswordPreference) {
+ String tmp = "";
+ for (int i = 0; i < p.mValue.length(); ++i) {
+ tmp += "*";
+ }
+ pref.setSummary(tmp);
+ } else {
+ pref.setSummary(p.mValue);
+ }
+ } else {
+ Log.i(TAG, "pref:"+p.mKey);
+ ((CheckBoxPreference) pref).setChecked(p.isChecked());
+ }
+ } else {
+ Log.w(TAG, "pref not found");
+ }
+ }
+ }
+
+ private void addPreferenceListener(AccountDetail details, OnPreferenceChangeListener listener) {
+ for (AccountDetail.PreferenceEntry p : details.getDetailValues()) {
+ Log.i(TAG, "addPreferenceListener: pref " + p.mKey + p.mValue);
+ Preference pref = findPreference(p.mKey);
+ if (pref != null) {
+ pref.setOnPreferenceChangeListener(listener);
+ } else {
+ Log.w(TAG, "addPreferenceListener: pref not found");
+ }
+ }
+ }
+
+ Preference.OnPreferenceChangeListener changeBasicPreferenceListener = new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+
+ Log.i(TAG, "Changing preference value:" + newValue);
+ if (preference instanceof CheckBoxPreference) {
+ mCallbacks.getAccount().getBasicDetails().setDetailString(preference.getKey(), newValue.toString());
+ } else {
+ if (preference instanceof PasswordPreference) {
+ String tmp = "";
+ for (int i = 0; i < ((String) newValue).length(); ++i) {
+ tmp += "*";
+ }
+ if(mCallbacks.getAccount().isSip())
+ mCallbacks.getAccount().getCredentials().get(0).setDetailString(preference.getKey(), newValue.toString());
+ preference.setSummary(tmp);
+ } else if(preference.getKey().contentEquals(AccountDetailBasic.CONFIG_ACCOUNT_USERNAME)) {
+ if(mCallbacks.getAccount().isSip()){
+ mCallbacks.getAccount().getCredentials().get(0).setDetailString(preference.getKey(), newValue.toString());
+ }
+ preference.setSummary((CharSequence) newValue);
+ } else {
+ preference.setSummary((CharSequence) newValue);
+ }
+
+ mCallbacks.getAccount().getBasicDetails().setDetailString(preference.getKey(), newValue.toString());
+ }
+ mCallbacks.getAccount().notifyObservers();
+ return true;
+ }
+ };
+
+}
diff --git a/ring-android/src/cx/ring/fragments/HistoryFragment.java b/ring-android/src/cx/ring/fragments/HistoryFragment.java
new file mode 100644
index 0000000..7a04f0a
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/HistoryFragment.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.fragments;
+
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import android.support.v4.app.ListFragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.AsyncTaskLoader;
+import android.support.v4.content.Loader;
+import android.view.*;
+import cx.ring.R;
+import cx.ring.adapters.ContactPictureTask;
+import cx.ring.client.DetailHistoryActivity;
+import cx.ring.history.HistoryManager;
+import cx.ring.loaders.HistoryLoader;
+import cx.ring.loaders.LoaderConstants;
+import cx.ring.history.HistoryEntry;
+import cx.ring.service.ISipService;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+public class HistoryFragment extends ListFragment implements LoaderManager.LoaderCallbacks<ArrayList<HistoryEntry>> {
+
+ private static final String TAG = HistoryFragment.class.getSimpleName();
+
+ HistoryAdapter mAdapter;
+ private Callbacks mCallbacks = sDummyCallbacks;
+ HistoryManager mHistoryManager;
+
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+ @Override
+ public void onCallHistory(HistoryEntry to) {
+ }
+
+ @Override
+ public ISipService getService() {
+ Log.i(TAG, "Dummy");
+ return null;
+ }
+
+ };
+
+ public static String ARGS = "Bundle.args";
+
+ public interface Callbacks {
+ public void onCallHistory(HistoryEntry to);
+
+ public ISipService getService();
+
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ Log.i(TAG, "Attaching HISTORY");
+ super.onAttach(activity);
+
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ inflater.inflate(R.menu.history, menu);
+ mHistoryManager = new HistoryManager(getActivity());
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_clear_history:
+ // TODO clean Database!
+ mHistoryManager.clearDB();
+ getLoaderManager().restartLoader(LoaderConstants.HISTORY_LOADER, null, this);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAdapter = new HistoryAdapter(getActivity(), new ArrayList<HistoryEntry>());
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
+ View inflatedView = inflater.inflate(R.layout.frag_history, parent, false);
+
+ return inflatedView;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+
+ super.onActivityCreated(savedInstanceState);
+ getListView().setAdapter(mAdapter);
+
+ getListView().setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1, int pos, long arg3) {
+
+ Bundle b = new Bundle();
+ b.putParcelable("entry", mAdapter.getItem(pos));
+ Intent toStart = new Intent(getActivity(), DetailHistoryActivity.class).putExtra(HistoryFragment.ARGS, b);
+ startActivity(toStart);
+
+ }
+ });
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Log.w(TAG, "onStart");
+ getLoaderManager().restartLoader(LoaderConstants.HISTORY_LOADER, null, this);
+ }
+
+ public void makeNewCall(int position) {
+ mCallbacks.onCallHistory(mAdapter.getItem(position));
+ }
+
+ public class HistoryAdapter extends BaseAdapter implements ListAdapter {
+
+ Context mContext;
+ ArrayList<HistoryEntry> dataset;
+ private ExecutorService infos_fetcher = Executors.newCachedThreadPool();
+
+ public HistoryAdapter(Context activity, ArrayList<HistoryEntry> history) {
+ mContext = activity;
+ dataset = history;
+ }
+
+ @Override
+ public View getView(final int pos, View convertView, ViewGroup arg2) {
+
+ HistoryView entryView;
+
+ if (convertView == null) {
+ // Get a new instance of the row layout view
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ convertView = inflater.inflate(R.layout.item_history, null);
+
+ // Hold the view objects in an object
+ // so they don't need to be re-fetched
+ entryView = new HistoryView();
+ entryView.photo = (ImageButton) convertView.findViewById(R.id.photo);
+ entryView.displayName = (TextView) convertView.findViewById(R.id.display_name);
+ entryView.date = (TextView) convertView.findViewById(R.id.date_start);
+ entryView.incoming = (TextView) convertView.findViewById(R.id.incomings);
+ entryView.outgoing = (TextView) convertView.findViewById(R.id.outgoings);
+ entryView.replay = (Button) convertView.findViewById(R.id.replay);
+ convertView.setTag(entryView);
+ } else {
+ entryView = (HistoryView) convertView.getTag();
+ }
+
+ // Transfer the stock data from the data object
+ // to the view objects
+
+ // SipCall call = (SipCall) mCallList.values().toArray()[position];
+ entryView.displayName.setText(dataset.get(pos).getContact().getmDisplayName());
+ infos_fetcher.execute(new ContactPictureTask(mContext, entryView.photo, dataset.get(pos).getContact()));
+
+ entryView.incoming.setText(getString(R.string.hist_in_calls, dataset.get(pos).getIncoming_sum()));
+ entryView.outgoing.setText(getString(R.string.hist_out_calls, dataset.get(pos).getOutgoing_sum()));
+
+ /*if (dataset.get(pos).getCalls().lastEntry().getValue().getRecordPath().length() > 0) {
+ entryView.replay.setVisibility(View.VISIBLE);
+ entryView.replay.setTag(R.id.replay, true);
+ entryView.replay.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ try {
+ if ((Boolean) v.getTag(R.id.replay)) {
+ mCallbacks.getService().startRecordedFilePlayback(dataset.get(pos).getCalls().lastEntry().getValue().getRecordPath());
+ v.setTag(R.id.replay, false);
+ ((Button) v).setText(getString(R.string.hist_replay_button_stop));
+ } else {
+ mCallbacks.getService().stopRecordedFilePlayback(dataset.get(pos).getCalls().lastEntry().getValue().getRecordPath());
+ v.setTag(R.id.replay, true);
+ ((Button) v).setText(getString(R.string.hist_replay_button));
+ }
+ } catch (RemoteException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ });
+ }*/
+
+ /*entryView.date.setText(dataset.get(pos).getCalls().lastEntry().getValue().getDate());*/
+ entryView.photo.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ makeNewCall(pos);
+
+ }
+ });
+
+ return convertView;
+
+ }
+
+ /**
+ * ******************
+ * ViewHolder Pattern
+ * *******************
+ */
+ public class HistoryView {
+ public ImageButton photo;
+ protected TextView displayName;
+ protected TextView date;
+ private Button replay;
+ private TextView outgoing;
+ private TextView incoming;
+ }
+
+ @Override
+ public int getCount() {
+
+ return dataset.size();
+ }
+
+ @Override
+ public HistoryEntry getItem(int pos) {
+ return dataset.get(pos);
+ }
+
+ @Override
+ public long getItemId(int arg0) {
+ return 0;
+ }
+
+ public void clear() {
+ dataset.clear();
+
+ }
+
+ public void addAll(ArrayList<HistoryEntry> history) {
+ dataset.addAll(history);
+ }
+
+ }
+
+ @Override
+ public AsyncTaskLoader<ArrayList<HistoryEntry>> onCreateLoader(int arg0, Bundle arg1) {
+ HistoryLoader loader = new HistoryLoader(getActivity());
+ loader.forceLoad();
+ return loader;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<ArrayList<HistoryEntry>> arrayListLoader, ArrayList<HistoryEntry> historyEntries) {
+ mAdapter.clear();
+ mAdapter.addAll(historyEntries);
+ mAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onLoaderReset(Loader<ArrayList<HistoryEntry>> arrayListLoader) {
+
+ }
+
+
+}
diff --git a/ring-android/src/cx/ring/fragments/HomeFragment.java b/ring-android/src/cx/ring/fragments/HomeFragment.java
new file mode 100644
index 0000000..e3066c9
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/HomeFragment.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.fragments;
+
+import android.support.v4.app.Fragment;
+import cx.ring.R;
+import cx.ring.adapters.SectionsPagerAdapter;
+import cx.ring.views.PagerSlidingTabStrip;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.view.ViewPager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class HomeFragment extends Fragment {
+ static final String TAG = HomeFragment.class.getSimpleName();
+
+ /**
+ * The {@link ViewPager} that will host the section contents.
+ */
+ ViewPager mViewPager;
+ SectionsPagerAdapter mSectionsPagerAdapter;
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ getActivity().getActionBar().setTitle(R.string.menu_item_home);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ }
+
+ @Override
+ public void onCreate(Bundle savedBundle) {
+ super.onCreate(savedBundle);
+ mSectionsPagerAdapter = new SectionsPagerAdapter(getActivity(), getChildFragmentManager());
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.frag_home, container, false);
+
+ // Set up the ViewPager with the sections adapter.
+ mViewPager = (ViewPager) rootView.findViewById(R.id.pager);
+ mViewPager.setPageTransformer(true, new ZoomOutPageTransformer(0.7f));
+
+ mViewPager.setOffscreenPageLimit(2);
+ mViewPager.setAdapter(mSectionsPagerAdapter);
+ mViewPager.setCurrentItem(1);
+
+ final PagerSlidingTabStrip strip = PagerSlidingTabStrip.class.cast(rootView.findViewById(R.id.pts_main));
+
+ strip.setViewPager(mViewPager);
+
+ return rootView;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ public class ZoomOutPageTransformer implements ViewPager.PageTransformer {
+ private static final float MIN_ALPHA = .6f;
+
+ public ZoomOutPageTransformer(float scalingStart) {
+ super();
+ }
+
+ @Override
+ public void transformPage(View page, float position) {
+ final float normalizedposition = Math.abs(Math.abs(position) - 1);
+ page.setAlpha(MIN_ALPHA + (1.f - MIN_ALPHA) * normalizedposition);
+ }
+ }
+
+ public SectionsPagerAdapter getSectionsPagerAdapter() {
+ return mSectionsPagerAdapter;
+ }
+
+ public ViewPager getViewPager() {
+ return mViewPager;
+ }
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/fragments/IMFragment.java b/ring-android/src/cx/ring/fragments/IMFragment.java
new file mode 100644
index 0000000..0769d63
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/IMFragment.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.fragments;
+
+import android.widget.*;
+import cx.ring.R;
+import cx.ring.adapters.DiscussArrayAdapter;
+import cx.ring.model.Conference;
+import cx.ring.model.SipMessage;
+import cx.ring.service.ISipService;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.widget.TextView.OnEditorActionListener;
+
+public class IMFragment extends CallableWrapperFragment {
+ static final String TAG = IMFragment.class.getSimpleName();
+
+ private Callbacks mCallbacks = sDummyCallbacks;
+
+ DiscussArrayAdapter mAdapter;
+ ListView list;
+
+ private EditText sendTextField;
+
+ @Override
+ public void onCreate(Bundle savedBundle) {
+ super.onCreate(savedBundle);
+
+ mAdapter = new DiscussArrayAdapter(getActivity(), getArguments());
+
+ }
+
+ @Override
+ public void incomingText(Conference updated, String ID, String from, String msg) {
+ mCallbacks.updateDisplayedConference(updated);
+ if(updated.equals(mCallbacks.getDisplayedConference())){
+ SipMessage sipMsg = new SipMessage(true, msg);
+ putMessage(sipMsg);
+ }
+
+ }
+
+
+ /**
+ * A dummy implementation of the {@link Callbacks} interface that does nothing. Used only when this fragment is not attached to an activity.
+ */
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+
+ @Override
+ public ISipService getService() {
+ return null;
+ }
+
+ @Override
+ public Conference getDisplayedConference() {
+ return null;
+ }
+
+ @Override
+ public boolean sendIM(SipMessage msg) {
+ return false;
+ }
+
+ @Override
+ public void updateDisplayedConference(Conference c) {
+
+ }
+
+ };
+
+ /**
+ * The Activity calling this fragment has to implement this interface
+ */
+ public interface Callbacks {
+ public ISipService getService();
+
+ public Conference getDisplayedConference();
+
+ public boolean sendIM(SipMessage msg);
+
+ public void updateDisplayedConference(Conference c);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.frag_imessaging, container, false);
+
+ list = (ListView) rootView.findViewById(R.id.message_list);
+ list.setAdapter(mAdapter);
+
+ sendTextField = (EditText) rootView.findViewById(R.id.send_im_edittext);
+
+ sendTextField.setOnEditorActionListener(new OnEditorActionListener() {
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+
+ if (actionId == EditorInfo.IME_ACTION_SEND) {
+ sendMessage();
+ }
+ return true;
+ }
+ });
+
+ rootView.findViewById(R.id.send_im_button).setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ sendMessage();
+ }
+ });
+
+
+ return rootView;
+ }
+
+ private void sendMessage() {
+ if (sendTextField.getText().toString().length() > 0) {
+ SipMessage toSend = new SipMessage(false, sendTextField.getText().toString());
+ if (mCallbacks.sendIM(toSend)) {
+ putMessage(toSend);
+ sendTextField.setText("");
+ } else {
+ Toast.makeText(getActivity(), "Error sending message", Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ public void putMessage(SipMessage msg) {
+ mAdapter.add(msg);
+ Log.i(TAG, "Messages" + mAdapter.getCount());
+ }
+}
diff --git a/ring-android/src/cx/ring/fragments/MenuFragment.java b/ring-android/src/cx/ring/fragments/MenuFragment.java
new file mode 100644
index 0000000..d7653d8
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/MenuFragment.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.fragments;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.AsyncTaskLoader;
+import android.support.v4.content.Loader;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.*;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemSelectedListener;
+import cx.ring.R;
+import cx.ring.adapters.AccountSelectionAdapter;
+import cx.ring.adapters.ContactPictureTask;
+import cx.ring.loaders.AccountsLoader;
+import cx.ring.loaders.LoaderConstants;
+import cx.ring.model.account.Account;
+import cx.ring.model.CallContact;
+import cx.ring.service.ISipService;
+
+import java.util.ArrayList;
+
+public class MenuFragment extends AccountWrapperFragment implements LoaderManager.LoaderCallbacks<Bundle> {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = MenuFragment.class.getSimpleName();
+
+ AccountSelectionAdapter mAccountAdapter;
+ private Spinner spinnerAccounts;
+ private Callbacks mCallbacks = sDummyCallbacks;
+
+ private ListView sections;
+
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+
+ @Override
+ public ISipService getService() {
+ return null;
+ }
+
+ @Override
+ public void onSectionSelected(int pos) {
+
+ }
+ };
+
+ public Account retrieveAccountById(String accountID) {
+ Account toReturn;
+ toReturn = mAccountAdapter.getAccount(accountID);
+
+ if(toReturn == null || !toReturn.isRegistered())
+ return getSelectedAccount();
+
+ return toReturn;
+ }
+
+ public interface Callbacks {
+
+ public ISipService getService();
+
+ public void onSectionSelected(int pos);
+
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+ mCallbacks = (Callbacks) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ public void onResume() {
+ super.onResume();
+
+ Log.i(TAG, "Resuming");
+ getLoaderManager().restartLoader(LoaderConstants.ACCOUNTS_LOADER, null, this);
+
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ getLoaderManager().restartLoader(LoaderConstants.ACCOUNTS_LOADER, null, this);
+ }
+
+
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
+ View inflatedView = inflater.inflate(R.layout.frag_menu, parent, false);
+
+ ArrayAdapter<String> paramAdapter = new ArrayAdapter<String>(getActivity(), R.layout.item_menu, getResources().getStringArray(
+ R.array.menu_items_param));
+ sections = (ListView) inflatedView.findViewById(R.id.listView);
+ sections.setAdapter(paramAdapter);
+ backToHome();
+ sections.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View selected, int pos, long arg3) {
+ mCallbacks.onSectionSelected(pos);
+ }
+ });
+
+ spinnerAccounts = (Spinner) inflatedView.findViewById(R.id.account_selection);
+ mAccountAdapter = new AccountSelectionAdapter(getActivity(), new ArrayList<Account>());
+ spinnerAccounts.setAdapter(mAccountAdapter);
+ spinnerAccounts.setOnItemSelectedListener(new OnItemSelectedListener() {
+
+ @Override
+ public void onItemSelected(AdapterView<?> arg0, View view, int pos, long arg3) {
+ mAccountAdapter.setSelectedAccount(pos);
+ view.findViewById(R.id.account_selected).setVisibility(View.GONE);
+ try {
+ mCallbacks.getService().setAccountOrder(mAccountAdapter.getAccountOrder());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> arg0) {
+ mAccountAdapter.setSelectedAccount(-1);
+ }
+ });
+
+ CallContact user = CallContact.ContactBuilder.buildUserContact(getActivity().getContentResolver());
+ new ContactPictureTask(getActivity(), (ImageView) inflatedView.findViewById(R.id.user_photo), user).run();
+
+ ((TextView) inflatedView.findViewById(R.id.user_name)).setText(user.getmDisplayName());
+
+ return inflatedView;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ }
+
+ public Account getSelectedAccount() {
+ return mAccountAdapter.getSelectedAccount();
+ }
+
+ public void updateAllAccounts() {
+ if (getActivity() != null)
+ getLoaderManager().restartLoader(LoaderConstants.ACCOUNTS_LOADER, null, this);
+ }
+
+ @Override
+ public void accountsChanged() {
+ updateAllAccounts();
+
+ }
+
+ @Override
+ public void accountStateChanged(String accoundID, String state, int code) {
+ if (mAccountAdapter != null)
+ mAccountAdapter.updateAccount(accoundID, state, code);
+ }
+
+ @Override
+ public AsyncTaskLoader<Bundle> onCreateLoader(int arg0, Bundle arg1) {
+ AccountsLoader l = new AccountsLoader(getActivity(), mCallbacks.getService());
+ l.forceLoad();
+ return l;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Bundle> loader, Bundle data) {
+ mAccountAdapter.removeAll();
+ ArrayList<Account> accounts = data.getParcelableArrayList(AccountsLoader.ACCOUNTS);
+ mAccountAdapter.addAll(accounts);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Bundle> loader) {
+
+ }
+
+ public void backToHome() {
+ sections.setItemChecked(0, true);
+ }
+
+}
diff --git a/ring-android/src/cx/ring/fragments/NestedSettingsFragment.java b/ring-android/src/cx/ring/fragments/NestedSettingsFragment.java
new file mode 100644
index 0000000..81a66ea
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/NestedSettingsFragment.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.fragments;
+
+import android.content.Intent;
+import android.os.RemoteException;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import cx.ring.model.account.CredentialsManager;
+import cx.ring.model.account.SRTPManager;
+import cx.ring.model.account.TLSManager;
+import cx.ring.model.account.Account;
+import cx.ring.service.ISipService;
+
+import java.util.ArrayList;
+
+public class NestedSettingsFragment extends PreferenceFragment {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = AdvancedAccountFragment.class.getSimpleName();
+
+ private Callbacks mCallbacks = sDummyCallbacks;
+
+ CredentialsManager mCredsManager;
+ SRTPManager mSrtpManager;
+ TLSManager mTlsManager;
+
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+
+ @Override
+ public Account getAccount() {
+ return null;
+ }
+
+ @Override
+ public ISipService getService() {
+ return null;
+ }
+
+ };
+
+ public String[] getTlsMethods() {
+ ArrayList<String> methods = null;
+ try {
+ methods = (ArrayList<String>) mCallbacks.getService().getTlsSupportedMethods();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ String[] results = new String[methods.size()];
+ methods.toArray(results);
+ return results;
+ }
+
+ public boolean checkCertificate(String crt) {
+ try {
+ return mCallbacks.getService().checkCertificateValidity(crt);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+ public boolean findRSAKey(String pemPath) {
+ try {
+ return mCallbacks.getService().checkForPrivateKey(pemPath);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+ public interface Callbacks {
+
+ public Account getAccount();
+
+ public ISipService getService();
+
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+ mCallbacks = (Callbacks) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setHasOptionsMenu(true);
+
+ // Load the preferences from an XML resource
+ switch (getArguments().getInt("MODE")) {
+ case 0: // Credentials
+ addPreferencesFromResource(cx.ring.R.xml.account_credentials);
+ mCredsManager = new CredentialsManager();
+ mCredsManager.onCreate(getActivity(), getPreferenceScreen(), mCallbacks.getAccount());
+ mCredsManager.reloadCredentials();
+ mCredsManager.setAddCredentialListener();
+ break;
+ case 1: // SRTP
+ mSrtpManager = new SRTPManager();
+ if (mCallbacks.getAccount().hasSDESEnabled()) { // SDES
+ addPreferencesFromResource(cx.ring.R.xml.account_sdes);
+ mSrtpManager.onCreate(getPreferenceScreen(), mCallbacks.getAccount());
+ mSrtpManager.setSDESListener();
+ } else { // ZRTP
+ addPreferencesFromResource(cx.ring.R.xml.account_zrtp);
+ mSrtpManager.onCreate(getPreferenceScreen(), mCallbacks.getAccount());
+ mSrtpManager.setZRTPListener();
+ }
+ break;
+ case 2:
+ addPreferencesFromResource(cx.ring.R.xml.account_tls);
+ mTlsManager = new TLSManager();
+ mTlsManager.onCreate(this, getPreferenceScreen(), mCallbacks.getAccount());
+ mTlsManager.setTLSListener();
+ break;
+ }
+
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ view.setBackgroundColor(getResources().getColor(android.R.color.white));
+ return view;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (mTlsManager != null) {
+ mTlsManager.onActivityResult(requestCode, resultCode, data);
+ }
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/fragments/SecurityAccountFragment.java b/ring-android/src/cx/ring/fragments/SecurityAccountFragment.java
new file mode 100644
index 0000000..01042e6
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/SecurityAccountFragment.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.fragments;
+
+import java.util.Locale;
+
+import cx.ring.R;
+import android.app.Activity;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceFragment;
+import cx.ring.model.account.AccountDetail;
+import cx.ring.model.account.AccountDetailSrtp;
+import cx.ring.model.account.Account;
+
+public class SecurityAccountFragment extends PreferenceFragment {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = SecurityAccountFragment.class.getSimpleName();
+
+ private Callbacks mCallbacks = sDummyCallbacks;
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+
+ @Override
+ public Account getAccount() {
+ return null;
+ }
+
+ @Override
+ public void displayCredentialsScreen() {
+ }
+
+ @Override
+ public void displaySRTPScreen() {
+ }
+
+ @Override
+ public void displayTLSScreen() {
+ }
+
+ };
+
+ public interface Callbacks {
+
+ public Account getAccount();
+
+ public void displayCredentialsScreen();
+
+ public void displaySRTPScreen();
+
+ public void displayTLSScreen();
+
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if(mCallbacks.getAccount().getTlsDetails().getDetailBoolean("TLS.enable")){
+ findPreference("TLS.details").setSummary(getString(R.string.account_tls_enabled_label));
+ } else {
+ findPreference("TLS.details").setSummary(getString(R.string.account_tls_disabled_label));
+ }
+
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.account_security_prefs);
+ updateSummaries();
+ findPreference("Credential.count").setOnPreferenceClickListener(new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ mCallbacks.displayCredentialsScreen();
+ return false;
+ }
+ });
+
+ setSrtpPreferenceDetails(mCallbacks.getAccount().getSrtpDetails());
+ addPreferenceListener(mCallbacks.getAccount().getSrtpDetails(), changeSrtpModeListener);
+
+ findPreference("TLS.details").setOnPreferenceClickListener(new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ mCallbacks.displayTLSScreen();
+ return false;
+ }
+ });
+
+ }
+
+ public void updateSummaries() {
+ findPreference("Credential.count").setSummary("" + mCallbacks.getAccount().getCredentials().size());
+ if(mCallbacks.getAccount().getTlsDetails().getDetailBoolean("TLS.enable")){
+ findPreference("TLS.details").setSummary(getString(R.string.account_tls_enabled_label));
+ } else {
+ findPreference("TLS.details").setSummary(getString(R.string.account_tls_disabled_label));
+ }
+ }
+
+ private void setSrtpPreferenceDetails(AccountDetailSrtp details) {
+
+ if (details.getDetailBoolean(AccountDetailSrtp.CONFIG_SRTP_ENABLE)) {
+ findPreference(AccountDetailSrtp.CONFIG_SRTP_ENABLE).setSummary(
+ details.getDetailString(AccountDetailSrtp.CONFIG_SRTP_KEY_EXCHANGE).toUpperCase(Locale.getDefault()));
+
+ } else {
+ findPreference(AccountDetailSrtp.CONFIG_SRTP_ENABLE).setSummary(getResources().getString(R.string.account_srtp_deactivated));
+
+ }
+
+ findPreference("SRTP.details").setEnabled(details.getDetailBoolean(AccountDetailSrtp.CONFIG_SRTP_ENABLE));
+ }
+
+ private void addPreferenceListener(AccountDetail details, OnPreferenceChangeListener listener) {
+
+ findPreference(AccountDetailSrtp.CONFIG_SRTP_ENABLE).setOnPreferenceChangeListener(changeSrtpModeListener);
+ findPreference("SRTP.details").setOnPreferenceClickListener(new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ mCallbacks.displaySRTPScreen();
+ return false;
+ }
+ });
+
+ }
+
+ Preference.OnPreferenceChangeListener changeSrtpModeListener = new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+
+ if (((String) newValue).contentEquals("NONE")) {
+ mCallbacks.getAccount().getSrtpDetails().setDetailString(AccountDetailSrtp.CONFIG_SRTP_ENABLE, AccountDetail.FALSE_STR);
+ preference.setSummary(getResources().getString(R.string.account_srtp_deactivated));
+ } else {
+ mCallbacks.getAccount().getSrtpDetails().setDetailString(AccountDetailSrtp.CONFIG_SRTP_ENABLE, AccountDetail.TRUE_STR);
+ mCallbacks.getAccount().getSrtpDetails()
+ .setDetailString(AccountDetailSrtp.CONFIG_SRTP_KEY_EXCHANGE, ((String) newValue).toLowerCase(Locale.getDefault()));
+ preference.setSummary(((String) newValue));
+ }
+ findPreference("SRTP.details").setEnabled(!((String) newValue).contentEquals("NONE"));
+ mCallbacks.getAccount().notifyObservers();
+ return true;
+ }
+ };
+
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/fragments/TransferDFragment.java b/ring-android/src/cx/ring/fragments/TransferDFragment.java
new file mode 100644
index 0000000..9d51c78
--- /dev/null
+++ b/ring-android/src/cx/ring/fragments/TransferDFragment.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.fragments;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.app.Dialog;
+import android.support.v4.app.DialogFragment;
+import cx.ring.R;
+import cx.ring.loaders.ContactsLoader;
+import cx.ring.model.Conference;
+import cx.ring.model.SipCall;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnShowListener;
+import android.content.Intent;
+import android.content.Loader;
+import android.location.Address;
+import android.location.Geocoder;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class TransferDFragment extends DialogFragment implements LoaderManager.LoaderCallbacks<Bundle> {
+ public static final int RESULT_TRANSFER_CONF = Activity.RESULT_FIRST_USER + 1;
+ public static final int RESULT_TRANSFER_NUMBER = Activity.RESULT_FIRST_USER + 2;
+
+ private AutoCompleteTextView mEditText;
+ private AutoCompleteAdapter autoCompleteAdapter;
+ SimpleCallListAdapter mAdapter;
+
+ /**
+ * Create a new instance of CallActionsDFragment
+ */
+ static TransferDFragment newInstance() {
+ TransferDFragment f = new TransferDFragment();
+ return f;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Pick a style based on the num.
+ int style = DialogFragment.STYLE_NORMAL, theme = 0;
+ setStyle(style, theme);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ View rootView = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_transfer, null);
+
+ ArrayList<Conference> calls = getArguments().getParcelableArrayList("calls");
+ final SipCall call_selected = getArguments().getParcelable("call_selected");
+
+ mAdapter = new SimpleCallListAdapter(getActivity(), calls);
+ ListView list = (ListView) rootView.findViewById(R.id.concurrent_calls);
+ list.setAdapter(mAdapter);
+ list.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1, int pos, long arg3) {
+
+ Intent in = new Intent();
+ in.putExtra("target", mAdapter.getItem(pos));
+ in.putExtra("transfer", call_selected);
+
+ getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_TRANSFER_CONF, in);
+ dismiss();
+ }
+ });
+ list.setEmptyView(rootView.findViewById(R.id.empty_view));
+
+ mEditText = (AutoCompleteTextView) rootView.findViewById(R.id.external_number);
+ mEditText.setAdapter(autoCompleteAdapter);
+
+ final AlertDialog a = new AlertDialog.Builder(getActivity()).setView(rootView)
+ .setTitle("Transfer " + call_selected.getmContact().getmDisplayName())
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+
+ }
+ }).setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_CANCELED, new Intent());
+ dismiss();
+ }
+ }).create();
+
+ a.setOnShowListener(new OnShowListener() {
+
+ @Override
+ public void onShow(DialogInterface dialog) {
+ Button b = a.getButton(AlertDialog.BUTTON_POSITIVE);
+ b.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View view) {
+ if(mEditText.getText().length() == 0){
+ Toast.makeText(getActivity(), "Enter a number to transfer this call", Toast.LENGTH_SHORT).show();
+ } else {
+ Intent in = new Intent();
+ in.putExtra("to_number", mEditText.getText().toString());
+ in.putExtra("transfer", call_selected);
+ getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_TRANSFER_NUMBER, in);
+ dismiss();
+ }
+ }
+ });
+
+ }
+ });
+ return a;
+ }
+
+ @Override
+ public Loader<Bundle> onCreateLoader(int id, Bundle args) {
+ Uri baseUri;
+
+ if (args != null) {
+ baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(args.getString("filter")));
+ } else {
+ baseUri = Contacts.CONTENT_URI;
+ }
+ ContactsLoader l = new ContactsLoader(getActivity(), baseUri);
+ l.forceLoad();
+ return l;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Bundle> loader, Bundle data) {
+
+// ArrayList<CallContact> tmp = data.getParcelableArrayList("Contacts");
+
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Bundle> loader) {
+ // Thi is called when the last Cursor provided to onLoadFinished
+ // mListAdapter.swapCursor(null);
+ }
+
+ private class AutoCompleteAdapter extends ArrayAdapter<Address> implements Filterable {
+
+ private LayoutInflater mInflater;
+ private Geocoder mGeocoder;
+// private StringBuilder mSb = new StringBuilder();
+
+ public AutoCompleteAdapter(final Context context) {
+ super(context, -1);
+ mInflater = LayoutInflater.from(context);
+ mGeocoder = new Geocoder(context);
+ }
+
+ @Override
+ public View getView(final int position, final View convertView, final ViewGroup parent) {
+ final TextView tv;
+ if (convertView != null) {
+ tv = (TextView) convertView;
+ } else {
+ tv = (TextView) mInflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);
+ }
+
+ return tv;
+ }
+
+ @Override
+ public Filter getFilter() {
+ Filter myFilter = new Filter() {
+ @Override
+ protected FilterResults performFiltering(final CharSequence constraint) {
+ List<Address> addressList = null;
+ if (constraint != null) {
+ try {
+ addressList = mGeocoder.getFromLocationName((String) constraint, 5);
+ } catch (IOException e) {
+ }
+ }
+ if (addressList == null) {
+ addressList = new ArrayList<Address>();
+ }
+
+ final FilterResults filterResults = new FilterResults();
+ filterResults.values = addressList;
+ filterResults.count = addressList.size();
+
+ return filterResults;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void publishResults(final CharSequence contraint, final FilterResults results) {
+ clear();
+ for (Address address : (List<Address>) results.values) {
+ add(address);
+ }
+ if (results.count > 0) {
+ notifyDataSetChanged();
+ } else {
+ notifyDataSetInvalidated();
+ }
+ }
+
+ @Override
+ public CharSequence convertResultToString(final Object resultValue) {
+ return resultValue == null ? "" : ((Address) resultValue).getAddressLine(0);
+ }
+ };
+ return myFilter;
+ }
+ }
+
+ private class SimpleCallListAdapter extends BaseAdapter {
+
+ private LayoutInflater mInflater;
+ ArrayList<Conference> calls;
+
+ public SimpleCallListAdapter(final Context context, ArrayList<Conference> calls2) {
+ super();
+ mInflater = LayoutInflater.from(context);
+ calls = calls2;
+ }
+
+ @Override
+ public View getView(final int position, final View convertView, final ViewGroup parent) {
+ final TextView tv;
+ if (convertView != null) {
+ tv = (TextView) convertView;
+ } else {
+ tv = (TextView) mInflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);
+ }
+
+ tv.setText(calls.get(position).getParticipants().get(0).getmContact().getmDisplayName());
+ return tv;
+ }
+
+ @Override
+ public int getCount() {
+ return calls.size();
+ }
+
+ @Override
+ public Conference getItem(int pos) {
+ return calls.get(pos);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+ }
+}
diff --git a/ring-android/src/cx/ring/history/DatabaseHelper.java b/ring-android/src/cx/ring/history/DatabaseHelper.java
new file mode 100644
index 0000000..a623e98
--- /dev/null
+++ b/ring-android/src/cx/ring/history/DatabaseHelper.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+
+package cx.ring.history;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper;
+import com.j256.ormlite.dao.Dao;
+import com.j256.ormlite.support.ConnectionSource;
+import com.j256.ormlite.table.TableUtils;
+
+import java.sql.SQLException;
+
+/**
+ * Database helper class used to manage the creation and upgrading of your database. This class also usually provides
+ * the DAOs used by the other classes.
+ */
+public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
+
+ // name of the database file for your application -- change to something appropriate for your app
+ private static final String DATABASE_NAME = "history.db";
+ // any time you make changes to your database objects, you may have to increase the database version
+ private static final int DATABASE_VERSION = 2;
+
+ // the DAO object we use to access the SimpleData table
+ private Dao<HistoryCall, Integer> historyDao = null;
+
+ public DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ /**
+ * This is called when the database is first created. Usually you should call createTable statements here to create
+ * the tables that will store your data.
+ */
+ @Override
+ public void onCreate(SQLiteDatabase db, ConnectionSource connectionSource) {
+ try {
+ Log.i(DatabaseHelper.class.getName(), "onCreate");
+ TableUtils.createTable(connectionSource, HistoryCall.class);
+ } catch (SQLException e) {
+ Log.e(DatabaseHelper.class.getName(), "Can't create database", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * This is called when your application is upgraded and it has a higher version number. This allows you to adjust
+ * the various data to match the new version number.
+ */
+ @Override
+ public void onUpgrade(SQLiteDatabase db, ConnectionSource connectionSource, int oldVersion, int newVersion) {
+ try {
+ Log.i(DatabaseHelper.class.getName(), "onUpgrade");
+ TableUtils.dropTable(connectionSource, HistoryCall.class, true);
+ // after we drop the old databases, we create the new ones
+ onCreate(db, connectionSource);
+ } catch (SQLException e) {
+ Log.e(DatabaseHelper.class.getName(), "Can't drop databases", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns the Database Access Object (DAO) for our SimpleData class. It will create it or just give the cached
+ * value.
+ */
+ public Dao<HistoryCall, Integer> getHistoryDao() throws SQLException {
+ if (historyDao == null) {
+ historyDao = getDao(HistoryCall.class);
+ }
+ return historyDao;
+ }
+
+ /**
+ * Close the database connections and clear any cached DAOs.
+ */
+ @Override
+ public void close() {
+ super.close();
+ historyDao = null;
+ }
+}
diff --git a/ring-android/src/cx/ring/history/HistoryCall.java b/ring-android/src/cx/ring/history/HistoryCall.java
new file mode 100644
index 0000000..2e76729
--- /dev/null
+++ b/ring-android/src/cx/ring/history/HistoryCall.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+
+package cx.ring.history;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.j256.ormlite.field.DatabaseField;
+import cx.ring.model.SipCall;
+
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class HistoryCall implements Parcelable {
+
+ @DatabaseField(index = true, columnName="TIMESTAMP_START")
+ long call_start;
+ @DatabaseField
+ long call_end;
+ @DatabaseField
+ String number;
+ @DatabaseField
+ boolean missed;
+ @DatabaseField
+ int direction;
+ @DatabaseField
+ String recordPath;
+ @DatabaseField
+ String accountID;
+ @DatabaseField
+ long contactID;
+ @DatabaseField
+ String callID;
+
+ public String getAccountID() {
+ return accountID;
+ }
+
+ public long getContactID() {
+ return contactID;
+ }
+
+ public HistoryCall(SipCall call) {
+ call_start = call.getTimestampStart_();
+ call_end = call.getTimestampEnd_();
+ accountID = call.getAccount().getAccountID();
+ number = call.getmContact().getPhones().get(0).getNumber();
+ missed = call.isRinging() && call.isIncoming();
+ direction = call.getCallType();
+ recordPath = call.getRecordPath();
+ contactID = call.getmContact().getId();
+ callID = call.getCallId();
+ }
+
+ /* Needed by ORMLite */
+ public HistoryCall() {
+ }
+
+ public String getDirection() {
+ switch (direction) {
+ case SipCall.direction.CALL_TYPE_INCOMING:
+ return "CALL_TYPE_INCOMING";
+ case SipCall.direction.CALL_TYPE_OUTGOING:
+ return "CALL_TYPE_OUTGOING";
+ default:
+ return "CALL_TYPE_UNDETERMINED";
+ }
+ }
+
+ public String getDate() {
+ return HistoryTimeModel.timeToHistoryConst(call_start);
+ }
+
+ public String getStartString(String format) {
+ Timestamp stamp = new Timestamp(call_start * 1000); // in milliseconds
+ Date date = new Date(stamp.getTime());
+ SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.getDefault());
+ sdf.setTimeZone(TimeZone.getDefault());
+ return sdf.format(date);
+
+ }
+
+ public String getDurationString() {
+
+ long duration = call_end - call_start;
+ if (duration < 60)
+ return String.format(Locale.getDefault(), "%02d secs", duration);
+
+ if (duration < 3600)
+ return String.format(Locale.getDefault(), "%02d mins %02d secs", (duration % 3600) / 60, (duration % 60));
+
+ return String.format(Locale.getDefault(), "%d h %02d mins %02d secs", duration / 3600, (duration % 3600) / 60, (duration % 60));
+
+ }
+
+ public long getDuration() {
+ return call_end - call_start;
+ }
+
+ public String getRecordPath() {
+ return recordPath;
+ }
+
+ public String getNumber() {
+ return number;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(call_start);
+ dest.writeLong(call_end);
+ dest.writeString(accountID);
+ dest.writeString(number);
+ dest.writeByte((byte) (missed ? 1 : 0));
+ dest.writeInt(direction);
+ dest.writeString(recordPath);
+ dest.writeLong(contactID);
+ dest.writeString(callID);
+ }
+
+ public static final Parcelable.Creator<HistoryCall> CREATOR = new Parcelable.Creator<HistoryCall>() {
+ public HistoryCall createFromParcel(Parcel in) {
+ return new HistoryCall(in);
+ }
+
+ public HistoryCall[] newArray(int size) {
+ return new HistoryCall[size];
+ }
+ };
+
+ private HistoryCall(Parcel in) {
+ call_start = in.readLong();
+ call_end = in.readLong();
+ accountID = in.readString();
+ number = in.readString();
+ missed = in.readByte() == 1;
+ direction = in.readInt();
+ recordPath = in.readString();
+ contactID = in.readLong();
+ callID = in.readString();
+ }
+
+ public boolean hasRecord() {
+ return recordPath.length() > 0;
+ }
+
+ public boolean isIncoming() {
+ return direction == SipCall.direction.CALL_TYPE_INCOMING;
+ }
+
+ public boolean isMissed() {
+ return missed;
+ }
+
+}
diff --git a/ring-android/src/cx/ring/history/HistoryEntry.java b/ring-android/src/cx/ring/history/HistoryEntry.java
new file mode 100644
index 0000000..385355c
--- /dev/null
+++ b/ring-android/src/cx/ring/history/HistoryEntry.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.history;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import cx.ring.model.CallContact;
+
+import java.util.ArrayList;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+public class HistoryEntry implements Parcelable {
+
+ private CallContact contact;
+ private NavigableMap<Long, HistoryCall> calls;
+ private String accountID;
+ int missed_sum;
+ int outgoing_sum;
+ int incoming_sum;
+
+ public HistoryEntry(String account, CallContact c) {
+ contact = c;
+ calls = new TreeMap<Long, HistoryCall>();
+ accountID = account;
+ missed_sum = outgoing_sum = incoming_sum = 0;
+ }
+
+ public String getAccountID() {
+ return accountID;
+ }
+
+ public void setAccountID(String accountID) {
+ this.accountID = accountID;
+ }
+
+ public NavigableMap<Long, HistoryCall> getCalls() {
+ return calls;
+ }
+
+ public CallContact getContact() {
+ return contact;
+ }
+
+ public void setContact(CallContact contact) {
+ this.contact = contact;
+ }
+
+ /**
+ * Each call is associated with a contact.
+ * When adding a call to an HIstoryEntry, this methods also verifies if we can update
+ * the contact (if contact is Unknown, replace it)
+ *
+ * @param historyCall The call to put in this HistoryEntry
+ * @param linkedTo The associated CallContact
+ */
+ public void addHistoryCall(HistoryCall historyCall, CallContact linkedTo) {
+ calls.put(historyCall.call_start, historyCall);
+ if (historyCall.isIncoming()) {
+ ++incoming_sum;
+ } else {
+ ++outgoing_sum;
+ }
+ if (historyCall.isMissed())
+ missed_sum++;
+
+ if (contact.isUnknown() && !linkedTo.isUnknown())
+ setContact(linkedTo);
+ }
+
+ public String getNumber() {
+ return calls.lastEntry().getValue().number;
+ }
+
+ public String getTotalDuration() {
+ int duration = 0;
+ ArrayList<HistoryCall> all_calls = new ArrayList<HistoryCall>(calls.values());
+ for (HistoryCall all_call : all_calls) {
+ duration += all_call.getDuration();
+ }
+
+ if (duration < 60)
+ return duration + "s";
+
+ return duration / 60 + "min";
+ }
+
+ public int getMissed_sum() {
+ return missed_sum;
+ }
+
+ public int getOutgoing_sum() {
+ return outgoing_sum;
+ }
+
+ public int getIncoming_sum() {
+ return incoming_sum;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+
+ dest.writeParcelable(contact, 0);
+
+ dest.writeList(new ArrayList<HistoryCall>(calls.values()));
+ dest.writeList(new ArrayList<Long>(calls.keySet()));
+
+ dest.writeString(accountID);
+ dest.writeInt(missed_sum);
+ dest.writeInt(outgoing_sum);
+ dest.writeInt(incoming_sum);
+
+ }
+
+ public static final Parcelable.Creator<HistoryEntry> CREATOR = new Parcelable.Creator<HistoryEntry>() {
+ public HistoryEntry createFromParcel(Parcel in) {
+ return new HistoryEntry(in);
+ }
+
+ public HistoryEntry[] newArray(int size) {
+ return new HistoryEntry[size];
+ }
+ };
+
+ private HistoryEntry(Parcel in) {
+ contact = in.readParcelable(CallContact.class.getClassLoader());
+
+ ArrayList<HistoryCall> values = new ArrayList<HistoryCall>();
+ in.readList(values, HistoryCall.class.getClassLoader());
+
+ ArrayList<Long> keys = new ArrayList<Long>();
+ in.readList(keys, Long.class.getClassLoader());
+
+ calls = new TreeMap<Long, HistoryCall>();
+ for (int i = 0; i < keys.size(); ++i) {
+ calls.put(keys.get(i), values.get(i));
+ }
+
+ accountID = in.readString();
+ missed_sum = in.readInt();
+ outgoing_sum = in.readInt();
+ incoming_sum = in.readInt();
+ }
+
+}
diff --git a/ring-android/src/cx/ring/history/HistoryManager.java b/ring-android/src/cx/ring/history/HistoryManager.java
new file mode 100644
index 0000000..591cbf0
--- /dev/null
+++ b/ring-android/src/cx/ring/history/HistoryManager.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.history;
+
+import android.content.Context;
+import com.j256.ormlite.android.apptools.OpenHelperManager;
+import com.j256.ormlite.stmt.QueryBuilder;
+import com.j256.ormlite.table.TableUtils;
+import cx.ring.model.Conference;
+import cx.ring.model.SipCall;
+
+import java.sql.SQLException;
+import java.util.List;
+
+public class HistoryManager {
+
+ private Context mContext;
+ private DatabaseHelper historyDBHelper = null;
+
+ public HistoryManager(Context context) {
+ mContext = context;
+ getHelper();
+ }
+
+ public boolean insertNewEntry(Conference toInsert){
+ for (SipCall call : toInsert.getParticipants()) {
+ call.setTimestampEnd_(System.currentTimeMillis());
+ HistoryCall persistent = new HistoryCall(call);
+ try {
+ getHelper().getHistoryDao().create(persistent);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /*
+ * Necessary when user hang up a call in a Conference
+ * The call creates an HistoryCall, but the conference still goes on
+ */
+ public boolean insertNewEntry(SipCall toInsert){
+ return true;
+ }
+
+ /**
+ * Retrieve helper for our DB
+ */
+ private DatabaseHelper getHelper() {
+ if (historyDBHelper == null) {
+ historyDBHelper = OpenHelperManager.getHelper(mContext, DatabaseHelper.class);
+ }
+ return historyDBHelper;
+ }
+
+ public List<HistoryCall> getAll() throws SQLException {
+
+ QueryBuilder<HistoryCall, Integer> qb = getHelper().getHistoryDao().queryBuilder();
+ qb.orderBy("TIMESTAMP_START", true);
+
+ return getHelper().getHistoryDao().query(qb.prepare());
+ }
+
+ public boolean clearDB() {
+ try {
+ TableUtils.clearTable(getHelper().getConnectionSource(), HistoryCall.class);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/ring-android/src/cx/ring/history/HistoryTimeModel.java b/ring-android/src/cx/ring/history/HistoryTimeModel.java
new file mode 100644
index 0000000..14a9035
--- /dev/null
+++ b/ring-android/src/cx/ring/history/HistoryTimeModel.java
@@ -0,0 +1,137 @@
+package cx.ring.history;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Locale;
+
+public class HistoryTimeModel {
+
+ static ArrayList<String> timeCategories;
+
+ public interface HistoryTimeCategoryModel {
+ String TODAY = "Today"; // 0
+ String YESTERDAY = "Yesterday"; // 1
+ String TWO_DAYS = getDate(2, "MM/dd");// 2
+ String THREE_DAYS = getDate(3, "MM/dd");// 3
+ String FOUR_DAYS = getDate(4, "MM/dd");// 4
+ String FIVE_DAYS = getDate(5, "MM/dd");// 5
+ String SIX_DAYS = getDate(6, "MM/dd");// 6
+ String LAST_WEEK = "Last week"; // 7
+ String TWO_WEEKS = "Two weeks ago"; // 8
+ String THREE_WEEKS = "Three weeks ago"; // 9
+ String LAST_MONTH = "Last month"; // 10
+ String TWO_MONTH = "Two months ago"; // 11
+ String THREE_MONTH = "Three months ago"; // 12
+ String FOUR_MONTH = "Four months ago"; // 13
+ String FIVE_MONTH = "Five months ago"; // 14
+ String SIX_MONTH = "Six months ago"; // 15
+ String SEVEN_MONTH = "Seven months ago"; // 16
+ String EIGHT_MONTH = "Eight months ago"; // 17
+ String NINE_MONTH = "Nine months ago"; // 18
+ String TEN_MONTH = "Ten months ago"; // 19
+ String ELEVEN_MONTH = "Eleven months ago"; // 20
+ String TWELVE_MONTH = "Twelve months ago"; // 21
+ String LAST_YEAR = "Last year"; // 22
+ String LONG_TIME_AGO = "Very long time ago"; // 23
+ String NEVER = "Never"; // 24
+ }
+
+ private static final String TAG = HistoryManager.class.getSimpleName();
+
+ static Calendar removeDays(int ago) {
+ Calendar cal = Calendar.getInstance(Locale.getDefault());
+ int currentDay = cal.get(Calendar.DAY_OF_MONTH);
+ // Set the date to 2 days ago
+ cal.set(Calendar.DAY_OF_MONTH, currentDay - ago);
+ return cal;
+ }
+
+ static String getDate(int ago, String format) {
+ Calendar cal = removeDays(ago);
+ SimpleDateFormat objFormatter = new SimpleDateFormat(format, Locale.CANADA);
+ objFormatter.setTimeZone(cal.getTimeZone());
+
+ String result = objFormatter.format(cal.getTime());
+ cal.clear();
+ return result;
+ }
+
+ public static String timeToHistoryConst(long time) {
+
+ if(timeCategories == null){
+ initializeCategories();
+ }
+
+ long time2 = time;
+ long currentTime = Calendar.getInstance(Locale.getDefault()).getTime().getTime() / 1000; // in seconds
+
+ if (time < 0)
+ return HistoryTimeCategoryModel.NEVER;
+
+ // Check if part if the current Nychthemeron
+ if (currentTime - time <= 3600 * 24) // The future case would be a bug, but it have to be handled anyway or it will appear in
+ // "very long time ago"
+ return HistoryTimeCategoryModel.TODAY;
+
+ time2 -= time % (3600 * 24); // Reset to midnight
+ currentTime -= currentTime % (3600 * 24); // Reset to midnight
+ // Check for last week
+ if (currentTime - (6) * 3600 * 24 < time2) {
+ for (int i = 1; i < 7; i++) {
+ if (currentTime - ((i) * 3600 * 24) == time2)
+ return timeCategories.get(i); // Yesterday to Six_days_ago
+ }
+ }
+ // Check for last month
+ else if (currentTime - ((4) * 7 * 24 * 3600) < time2) {
+ for (int i = 1; i < 4; i++) {
+ if (currentTime - ((i + 1) * 7 * 24 * 3600) < time2)
+ return timeCategories.get(i + timeCategories.indexOf(HistoryTimeCategoryModel.LAST_WEEK) - 1); // Last_week to Three_weeks_ago
+ }
+ }
+ // Check for last year
+ else if (currentTime - (12) * 30.4f * 24 * 3600 < time2) {
+ for (int i = 1; i < 12; i++) {
+ if (currentTime - (i + 1) * 30.4f * 24 * 3600 < time2) // Not exact, but faster
+ return timeCategories.get(i + timeCategories.indexOf(HistoryTimeCategoryModel.LAST_MONTH) - 1);
+ // Last_month to Twelve_months ago
+ }
+ }
+ // if (QDate::currentDate().addYears(-1) >= date && QDate::currentDate().addYears(-2) < date)
+ else if (currentTime - 365 * 24 * 3600 < time2)
+ return HistoryTimeCategoryModel.LAST_YEAR;
+
+ // Every other senario
+ return HistoryTimeCategoryModel.LONG_TIME_AGO;
+ }
+
+ private static void initializeCategories() {
+ timeCategories = new ArrayList<String>();
+ timeCategories.add(HistoryTimeCategoryModel.TODAY);
+ timeCategories.add(HistoryTimeCategoryModel.YESTERDAY);
+ timeCategories.add(HistoryTimeCategoryModel.TWO_DAYS);
+ timeCategories.add(HistoryTimeCategoryModel.THREE_DAYS);
+ timeCategories.add(HistoryTimeCategoryModel.FOUR_DAYS);
+ timeCategories.add(HistoryTimeCategoryModel.FIVE_DAYS);
+ timeCategories.add(HistoryTimeCategoryModel.SIX_DAYS);
+ timeCategories.add(HistoryTimeCategoryModel.LAST_WEEK);
+ timeCategories.add(HistoryTimeCategoryModel.TWO_WEEKS);
+ timeCategories.add(HistoryTimeCategoryModel.THREE_WEEKS);
+ timeCategories.add(HistoryTimeCategoryModel.LAST_MONTH);
+ timeCategories.add(HistoryTimeCategoryModel.TWO_MONTH);
+ timeCategories.add(HistoryTimeCategoryModel.THREE_MONTH);
+ timeCategories.add(HistoryTimeCategoryModel.FOUR_MONTH);
+ timeCategories.add(HistoryTimeCategoryModel.FIVE_MONTH);
+ timeCategories.add(HistoryTimeCategoryModel.SIX_MONTH);
+ timeCategories.add(HistoryTimeCategoryModel.SEVEN_MONTH);
+ timeCategories.add(HistoryTimeCategoryModel.EIGHT_MONTH);
+ timeCategories.add(HistoryTimeCategoryModel.NINE_MONTH);
+ timeCategories.add(HistoryTimeCategoryModel.TEN_MONTH);
+ timeCategories.add(HistoryTimeCategoryModel.ELEVEN_MONTH);
+ timeCategories.add(HistoryTimeCategoryModel.TWELVE_MONTH);
+ timeCategories.add(HistoryTimeCategoryModel.LAST_YEAR);
+ timeCategories.add(HistoryTimeCategoryModel.LONG_TIME_AGO);
+ timeCategories.add(HistoryTimeCategoryModel.NEVER);
+ }
+}
diff --git a/ring-android/src/cx/ring/interfaces/AccountsInterface.java b/ring-android/src/cx/ring/interfaces/AccountsInterface.java
new file mode 100644
index 0000000..e441647
--- /dev/null
+++ b/ring-android/src/cx/ring/interfaces/AccountsInterface.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.interfaces;
+
+public interface AccountsInterface {
+
+ public void accountsChanged();
+
+ public void accountStateChanged(String accoundID, String state, int code);
+
+
+}
diff --git a/ring-android/src/cx/ring/interfaces/CallInterface.java b/ring-android/src/cx/ring/interfaces/CallInterface.java
new file mode 100644
index 0000000..c1cd902
--- /dev/null
+++ b/ring-android/src/cx/ring/interfaces/CallInterface.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.interfaces;
+
+
+import cx.ring.model.Conference;
+
+import java.util.HashMap;
+
+public interface CallInterface {
+
+ public void callStateChanged(Conference c, String callID, String state);
+
+ public void incomingText(Conference c, String ID, String from, String msg);
+
+ public void confCreated(Conference c, String id);
+
+ public void confRemoved(Conference c, String id);
+
+ public void confChanged(Conference c, String id, String state);
+
+ public void recordingChanged(Conference c, String callID, String filename);
+
+ public void secureZrtpOn(Conference c, String id);
+
+ public void secureZrtpOff(Conference c, String id);
+
+ public void displaySAS(Conference c, String securedCallID);
+
+ public void zrtpNegotiationFailed(Conference c, String securedCallID);
+
+ public void zrtpNotSupported(Conference c, String securedCallID);
+
+ public void rtcpReportReceived(Conference c, HashMap<String, Integer> stats);
+}
diff --git a/ring-android/src/cx/ring/loaders/AccountsLoader.java b/ring-android/src/cx/ring/loaders/AccountsLoader.java
new file mode 100644
index 0000000..8e038ff
--- /dev/null
+++ b/ring-android/src/cx/ring/loaders/AccountsLoader.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.loaders;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import android.support.v4.content.AsyncTaskLoader;
+import cx.ring.model.account.Account;
+import cx.ring.service.ISipService;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+public class AccountsLoader extends AsyncTaskLoader<Bundle> {
+
+ private static final String TAG = AccountsLoader.class.getSimpleName();
+ public static final String ACCOUNTS = "accounts";
+ public static final String ACCOUNT_IP2IP = "IP2IP";
+ ISipService service;
+ Bundle mData;
+
+ public AccountsLoader(Context context, ISipService ref) {
+ super(context);
+ service = ref;
+ }
+
+ /****************************************************/
+ /** (1) A task that performs the asynchronous load **/
+ /****************************************************/
+
+ @SuppressWarnings("unchecked")
+ // Hashmap runtime cast
+ @Override
+ public Bundle loadInBackground() {
+
+ ArrayList<Account> accounts = new ArrayList<Account>();
+ Account IP2IP = null;
+
+ try {
+ ArrayList<String> accountIDs = (ArrayList<String>) service.getAccountList();
+ HashMap<String, String> details;
+ ArrayList<HashMap<String, String>> credentials;
+ for (String id : accountIDs) {
+
+ if (id.contentEquals(ACCOUNT_IP2IP)) {
+ details = (HashMap<String, String>) service.getAccountDetails(id);
+ IP2IP = new Account(ACCOUNT_IP2IP, details, new ArrayList<HashMap<String, String>>()); // Empty credentials
+ continue;
+ }
+ details = (HashMap<String, String>) service.getAccountDetails(id);
+ credentials = (ArrayList<HashMap<String, String>>) service.getCredentials(id);
+ Account tmp = new Account(id, details, credentials);
+
+ accounts.add(tmp);
+
+ Log.i(TAG, "account:" + tmp.getAlias() + " " + tmp.isEnabled());
+
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ } catch (NullPointerException e1) {
+ Log.e(TAG, e1.toString());
+ }
+
+ Bundle result = new Bundle();
+ result.putParcelableArrayList(ACCOUNTS, accounts);
+ result.putParcelable(ACCOUNT_IP2IP, IP2IP);
+ return result;
+ }
+
+
+ /********************************************************/
+ /** (2) Deliver the results to the registered listener **/
+ /********************************************************/
+
+ @Override
+ public void deliverResult(Bundle data) {
+ if (isReset()) {
+ // The Loader has been reset; ignore the result and invalidate the data.
+ releaseResources(data);
+ return;
+ }
+
+ // Hold a reference to the old data so it doesn't get garbage collected.
+ // We must protect it until the new data has been delivered.
+ Bundle oldData = mData;
+ mData = data;
+
+ if (isStarted()) {
+ // If the Loader is in a started state, deliver the results to the
+ // client. The superclass method does this for us.
+ super.deliverResult(data);
+ }
+
+ // Invalidate the old data as we don't need it any more.
+ if (oldData != null && oldData != data) {
+ releaseResources(oldData);
+ }
+ }
+
+ /*********************************************************/
+ /** (3) Implement the Loader’s state-dependent behavior **/
+ /*********************************************************/
+
+ @Override
+ protected void onStartLoading() {
+ if (mData != null) {
+ // Deliver any previously loaded data immediately.
+ deliverResult(mData);
+ }
+
+ if (takeContentChanged() || mData == null) {
+ // When the observer detects a change, it should call onContentChanged()
+ // on the Loader, which will cause the next call to takeContentChanged()
+ // to return true. If this is ever the case (or if the current data is
+ // null), we force a new load.
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ // The Loader is in a stopped state, so we should attempt to cancel the
+ // current load (if there is one).
+ cancelLoad();
+
+ // Note that we leave the observer as is. Loaders in a stopped state
+ // should still monitor the data source for changes so that the Loader
+ // will know to force a new load if it is ever started again.
+ }
+
+ @Override
+ protected void onReset() {
+ // Ensure the loader has been stopped.
+ onStopLoading();
+
+ // At this point we can release the resources associated with 'mData'.
+ if (mData != null) {
+ releaseResources(mData);
+ mData = null;
+ }
+ }
+
+ @Override
+ public void onCanceled(Bundle data) {
+ // Attempt to cancel the current asynchronous load.
+ super.onCanceled(data);
+
+ // The load has been canceled, so we should release the resources
+ // associated with 'data'.
+ releaseResources(data);
+ }
+
+ private void releaseResources(Bundle data) {
+ // For a simple List, there is nothing to do. For something like a Cursor, we
+ // would close it in this method. All resources associated with the Loader
+ // should be released here.
+ }
+}
diff --git a/ring-android/src/cx/ring/loaders/ContactsLoader.java b/ring-android/src/cx/ring/loaders/ContactsLoader.java
new file mode 100644
index 0000000..3482d34
--- /dev/null
+++ b/ring-android/src/cx/ring/loaders/ContactsLoader.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.loaders;
+
+import java.util.ArrayList;
+
+import cx.ring.model.CallContact;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.Contacts;
+
+public class ContactsLoader extends AsyncTaskLoader<Bundle> {
+
+// private static final String TAG = ContactsLoader.class.getSimpleName();
+
+ // These are the Contacts rows that we will retrieve.
+ static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] { Contacts._ID, Contacts.DISPLAY_NAME, Contacts.PHOTO_ID, Contacts.LOOKUP_KEY, Contacts.STARRED };
+ static final String[] CONTACTS_PHONES_PROJECTION = new String[] { Phone.NUMBER, Phone.TYPE };
+ static final String[] CONTACTS_SIP_PROJECTION = new String[] { SipAddress.SIP_ADDRESS, SipAddress.TYPE };
+
+ String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND (" + Contacts.HAS_PHONE_NUMBER + "=1) AND (" + Contacts.DISPLAY_NAME + " != '' ))";
+ Uri baseUri;
+
+ public ContactsLoader(Context context, Uri u) {
+ super(context);
+ baseUri = u;
+ }
+
+ @Override
+ public Bundle loadInBackground() {
+ ArrayList<CallContact> contacts = new ArrayList<CallContact>();
+ ArrayList<CallContact> starred = new ArrayList<CallContact>();
+
+ Cursor result = getContext().getContentResolver().query(baseUri, CONTACTS_SUMMARY_PROJECTION, select, null,
+ Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
+ int iID = result.getColumnIndex(Contacts._ID);
+ int iName = result.getColumnIndex(Contacts.DISPLAY_NAME);
+ int iPhoto = result.getColumnIndex(Contacts.PHOTO_ID);
+ int iStarred = result.getColumnIndex(Contacts.STARRED);
+ CallContact.ContactBuilder builder = CallContact.ContactBuilder.getInstance();
+
+ while (result.moveToNext()) {
+ builder.startNewContact(result.getLong(iID), result.getString(iName), result.getLong(iPhoto));
+
+// Cursor cPhones = getContext().getContentResolver().query(Phone.CONTENT_URI, CONTACTS_PHONES_PROJECTION,
+// Phone.CONTACT_ID + " =" + result.getLong(iID), null, null);
+
+// while (cPhones.moveToNext()) {
+// builder.addPhoneNumber(cPhones.getString(cPhones.getColumnIndex(Phone.NUMBER)), cPhones.getInt(cPhones.getColumnIndex(Phone.TYPE)));
+//// Log.i(TAG,"Phone:"+cPhones.getString(cPhones.getColumnIndex(Phone.NUMBER)));
+// }
+// cPhones.close();
+//
+// Cursor cSip = getContext().getContentResolver().query(Phone.CONTENT_URI, CONTACTS_SIP_PROJECTION,
+// Phone.CONTACT_ID + "=" + result.getLong(iID), null, null);
+//
+// while (cSip.moveToNext()) {
+// builder.addSipNumber(cSip.getString(cSip.getColumnIndex(SipAddress.SIP_ADDRESS)), cSip.getInt(cSip.getColumnIndex(SipAddress.TYPE)));
+//// Log.i(TAG,"Phone:"+cSip.getString(cSip.getColumnIndex(SipAddress.SIP_ADDRESS)));
+// }
+// cSip.close();
+
+ contacts.add(builder.build());
+ if (result.getInt(iStarred) == 1) {
+ starred.add(builder.build());
+ }
+
+ }
+
+ result.close();
+ Bundle toReturn = new Bundle();
+
+ toReturn.putParcelableArrayList("Contacts", contacts);
+ toReturn.putParcelableArrayList("Starred", starred);
+
+ return toReturn;
+ }
+}
diff --git a/ring-android/src/cx/ring/loaders/HistoryLoader.java b/ring-android/src/cx/ring/loaders/HistoryLoader.java
new file mode 100644
index 0000000..5d878aa
--- /dev/null
+++ b/ring-android/src/cx/ring/loaders/HistoryLoader.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.ContactsContract;
+import android.support.v4.content.AsyncTaskLoader;
+import cx.ring.history.HistoryCall;
+import cx.ring.history.HistoryEntry;
+import cx.ring.history.HistoryManager;
+import cx.ring.model.CallContact;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class HistoryLoader extends AsyncTaskLoader<ArrayList<HistoryEntry>> {
+
+ private static final String TAG = HistoryLoader.class.getSimpleName();
+
+ private HistoryManager historyManager = null;
+
+ public HistoryLoader(Context context) {
+ super(context);
+ historyManager = new HistoryManager(context);
+ }
+
+ @SuppressWarnings("unchecked")
+ // Hashmap runtime cast
+ @Override
+ public ArrayList<HistoryEntry> loadInBackground() {
+
+ HashMap<String,HistoryEntry> historyEntries = new HashMap<String, HistoryEntry>();
+
+ try {
+ List<HistoryCall> list = historyManager.getAll();
+ CallContact.ContactBuilder builder = CallContact.ContactBuilder.getInstance();
+ for (HistoryCall call : list) {
+ CallContact contact;
+ if (call.getContactID() == CallContact.DEFAULT_ID) {
+ contact = CallContact.ContactBuilder.buildUnknownContact(call.getNumber());
+ } else {
+ Cursor result = getContext().getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null,
+ ContactsContract.Contacts._ID + " = ?",
+ new String[]{String.valueOf(call.getContactID())}, null);
+ int iID = result.getColumnIndex(ContactsContract.Contacts._ID);
+ int iName = result.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
+ int iPhoto = result.getColumnIndex(ContactsContract.Contacts.PHOTO_ID);
+
+ if (result.moveToFirst()) {
+ builder.startNewContact(result.getLong(iID), result.getString(iName), result.getLong(iPhoto));
+ builder.addPhoneNumber(call.getNumber(), 0);
+ contact = builder.build();
+ } else {
+ contact = CallContact.ContactBuilder.buildUnknownContact(call.getNumber());
+ }
+ result.close();
+ }
+
+ if (historyEntries.containsKey(call.getNumber())) {
+ // It's a direct match
+ historyEntries.get(call.getNumber()).addHistoryCall(call, contact);
+ } else {
+ // Maybe we can extract the extension @ account pattern
+ Pattern p = Pattern.compile("<sip:([^@]+)@([^>]+)>");
+ Matcher m = p.matcher(call.getNumber());
+ if (m.find()) {
+
+ if (historyEntries.containsKey(m.group(1) + "@" + m.group(2))) {
+ historyEntries.get(m.group(1) + "@" + m.group(2)).addHistoryCall(call, contact);
+ } else {
+ HistoryEntry e = new HistoryEntry(call.getAccountID(), contact);
+ e.addHistoryCall(call, contact);
+ historyEntries.put(m.group(1) + "@" + m.group(2), e);
+ }
+
+ } else {
+ HistoryEntry e = new HistoryEntry(call.getAccountID(), contact);
+ e.addHistoryCall(call, contact);
+ historyEntries.put(call.getNumber(), e);
+ }
+
+ }
+ }
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+
+/*
+ try {
+ ArrayList<HashMap<String, String>> history = (ArrayList<HashMap<String, String>>) service.getHistory();
+
+ for (HashMap<String, String> entry : history) {
+
+ CallContact contact;
+ String contactName = entry.get(ServiceConstants.history.DISPLAY_NAME_KEY);
+ String number_called = entry.get(ServiceConstants.history.PEER_NUMBER_KEY);
+ if (contactName.isEmpty()) {
+ contact = ContactBuilder.buildUnknownContact(number_called);
+ } else {
+ contact = ContactBuilder.getInstance().buildSimpleContact(contactName, number_called);
+ }
+
+ if (historyEntries.containsKey(number_called)) {
+ // It's a direct match
+ historyEntries.get(number_called).addHistoryCall(new HistoryCall(entry), contact);
+ } else {
+ // Maybe we can extract the extension @ account pattern
+ Pattern p = Pattern.compile("<sip:([^@]+)@([^>]+)>");
+ Matcher m = p.matcher(number_called);
+ if (m.find()) {
+
+ if (historyEntries.containsKey(m.group(1) + "@" + m.group(2))) {
+ historyEntries.get(m.group(1) + "@" + m.group(2)).addHistoryCall(new HistoryCall(entry), contact);
+ } else {
+ HistoryEntry e = new HistoryEntry(entry.get(ServiceConstants.history.ACCOUNT_ID_KEY), contact);
+ e.addHistoryCall(new HistoryCall(entry), contact);
+ historyEntries.put(m.group(1) + "@" + m.group(2), e);
+ }
+
+ } else {
+
+ HistoryEntry e = new HistoryEntry(entry.get(ServiceConstants.history.ACCOUNT_ID_KEY), contact);
+ e.addHistoryCall(new HistoryCall(entry), contact);
+ historyEntries.put(number_called, e);
+ }
+
+ }
+
+ }
+
+ } catch (RemoteException e) {
+ Log.i(TAG, e.toString());
+ }*/
+ return new ArrayList<HistoryEntry>(historyEntries.values());
+ }
+
+
+}
diff --git a/ring-android/src/cx/ring/loaders/LoaderConstants.java b/ring-android/src/cx/ring/loaders/LoaderConstants.java
new file mode 100644
index 0000000..2d71312
--- /dev/null
+++ b/ring-android/src/cx/ring/loaders/LoaderConstants.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.loaders;
+
+public class LoaderConstants {
+
+ public static final int CONTACT_LOADER = 0;
+ public static final int ACCOUNTS_LOADER = 1;
+ public static final int HISTORY_LOADER = 2;
+
+}
diff --git a/ring-android/src/cx/ring/model/Attractor.java b/ring-android/src/cx/ring/model/Attractor.java
new file mode 100644
index 0000000..8240c57
--- /dev/null
+++ b/ring-android/src/cx/ring/model/Attractor.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Adrien Beraud <adrien.beraud@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+public class Attractor {
+
+ public interface Callback {
+
+ /**
+ * Called when a bubble is on the "active" zone of the attractor.
+ *
+ * @param b The bubble that is on the attractor.
+ * @return true if the bubble should be removed from the model, false otherwise.
+ */
+ public boolean onBubbleSucked(Bubble b);
+ }
+
+ public enum Type {
+ POINT, BORDER
+ }
+
+ final Callback callback;
+ final Type type;
+
+ private final RectF bounds = new RectF();
+ private final RectF boundsScaled = new RectF();
+ final PointF pos = new PointF();
+ final float radius;
+ private final Bitmap img;
+ final String name;
+
+ public Attractor(PointF pos, float size, Callback callback, Bitmap img) {
+ this.type = Type.POINT;
+ this.callback = callback;
+ this.pos.set(pos);
+ this.radius = size/2;
+ this.img = img;
+ this.name = null;
+ setBounds();
+ }
+
+ public Attractor(PointF pos, float radius, Callback callback, Context c, int resId) {
+ this(pos, radius, callback, BitmapFactory.decodeResource(c.getResources(), resId));
+ }
+
+ public Attractor(String name, float size, Callback callback, Bitmap img) {
+ this.type = Type.POINT;
+ this.name = name;
+ this.callback = callback;
+ this.radius = size/2;
+ this.img = img;
+ setBounds();
+ }
+
+ public void setSize(float w, float h)
+ {
+ if (type != Type.BORDER)
+ return;
+ pos.set(w, h);
+ setBounds();
+ }
+
+ public void setPos(float x, float y) {
+ pos.set(x, y);
+ setBounds();
+ }
+
+ private void setBounds() {
+ bounds.set(pos.x - radius, pos.y - radius, pos.x + radius, pos.y + radius);
+ }
+
+ public RectF getBounds() {
+ return bounds;
+ }
+
+ public RectF getBounds(float scale) {
+ float r = radius * scale;
+ boundsScaled.set(pos.x - r, pos.y - r, pos.x + r, pos.y + r);
+ return boundsScaled;
+ }
+
+ public RectF getBounds(float scale, PointF start, float d) {
+ float r = radius * scale;
+ float md = 1.f - d;
+ float x = pos.x * d + start.x * md;
+ float y = pos.y * d + start.y * md;
+ boundsScaled.set(x - r, y - r, x + r, y + r);
+ return boundsScaled;
+ }
+
+ public Bitmap getBitmap() {
+ return img;
+ }
+
+}
diff --git a/ring-android/src/cx/ring/model/Bubble.java b/ring-android/src/cx/ring/model/Bubble.java
new file mode 100644
index 0000000..3cf6e8a
--- /dev/null
+++ b/ring-android/src/cx/ring/model/Bubble.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model;
+
+import android.content.Context;
+import android.graphics.*;
+import android.graphics.Paint.Style;
+import cx.ring.R;
+import cx.ring.adapters.ContactPictureTask;
+
+public abstract class Bubble {
+
+ protected PointF pos = new PointF();
+ protected RectF bounds;
+ private float targetScale = 1.f;
+ protected float radius;
+ protected float scale = .1f;
+ public PointF speed = new PointF(0, 0);
+ //public PointF last_speed = new PointF();
+ public final PointF attractionPoint;
+ public Attractor attractor = null;
+
+ public boolean isUser;
+
+ private boolean grabbed = false;
+ private long lastDrag;
+
+ public boolean markedToDie = false;
+ //public long lastTime = System.nanoTime();
+
+ // A Bitmap object that is going to be passed to the BitmapShader
+ protected Bitmap externalBMP;
+ protected Bitmap savedPhoto;
+
+ protected Context mContext;
+
+ public Bubble(Context context, CallContact contact, float x, float y, float size) {
+ mContext = context;
+ pos.set(x, y);
+ radius = size / 2; // 10 is the white stroke
+ savedPhoto = getContactPhoto(context, contact, (int) size);
+ generateBitmap();
+ attractionPoint = new PointF(x, y);
+ isUser = false;
+ }
+
+ public void update(float dt) {
+ setScale(scale + (targetScale - scale) * dt * 5.f);
+ }
+
+ public void grab() {
+ grabbed = true;
+ lastDrag = System.nanoTime();
+ targetScale = .8f;
+ }
+
+ public void ungrab() {
+ grabbed = false;
+ targetScale = 1.f;
+ }
+
+ public void close() {
+ markedToDie = true;
+ targetScale = .1f;
+ }
+
+ public void drag(float x, float y) {
+ long now = System.nanoTime();
+ float dt = (float) ((now - lastDrag) / 1000000000.);
+ float dx = x - pos.x, dy = y - pos.y;
+ lastDrag = now;
+ setPos(x, y);
+ speed.x = dx / dt;
+ speed.y = dy / dt;
+ }
+
+ public void setTargetScale(float t) {
+ targetScale = t;
+ }
+
+ public boolean isGrabbed() {
+ return grabbed;
+ }
+
+ protected void generateBitmap() {
+
+ int w = savedPhoto.getWidth(), h = savedPhoto.getHeight();
+ if (w > h) {
+ w = h;
+ } else if (h > w) {
+ h = w;
+ }
+ externalBMP = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ BitmapShader shader;
+ shader = new BitmapShader(savedPhoto, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+
+ Paint paint = new Paint();
+ paint.setDither(true);
+ paint.setAntiAlias(true);
+ paint.setShader(shader);
+ Canvas internalCanvas = new Canvas(externalBMP);
+ internalCanvas.drawCircle(w / 2, h / 2, w / 2, paint);
+
+ Paint mLines = new Paint();
+ mLines.setStyle(Style.STROKE);
+ mLines.setStrokeWidth(8);
+ mLines.setColor(Color.WHITE);
+
+ mLines.setDither(true);
+ mLines.setAntiAlias(true);
+ internalCanvas.drawCircle(w / 2, h / 2, w / 2 - 4, mLines);
+
+ bounds = new RectF(pos.x - getRadius(), pos.y - getRadius(), pos.x + getRadius(), pos.y + getRadius());
+ }
+
+ protected Bitmap getContactPhoto(Context context, CallContact contact, int size) {
+ if (contact.getPhoto_id() > 0) {
+ return ContactPictureTask.loadContactPhoto(context.getContentResolver(), contact.getId());
+ } else {
+ return ContactPictureTask.decodeSampledBitmapFromResource(context.getResources(), R.drawable.ic_contact_picture, size, size);
+ }
+ }
+
+ public Bitmap getBitmap() {
+ return externalBMP;
+ }
+
+ public RectF getBounds() {
+ return bounds;
+ }
+
+ public void set(float x, float y, float s) {
+ scale = s;
+ pos.x = x;
+ pos.y = y;
+ bounds.set(pos.x - getRadius(), pos.y - getRadius(), pos.x + getRadius(), pos.y + getRadius());
+ }
+
+ public float getPosX() {
+ return pos.x;
+ }
+
+ public float getPosY() {
+ return pos.y;
+ }
+
+ public void setPos(float x, float y) {
+ set(x, y, scale);
+ }
+
+ public PointF getPos() {
+ return pos;
+ }
+
+ public float getScale() {
+ return scale;
+ }
+
+ public void setScale(float s) {
+ set(pos.x, pos.y, s);
+ }
+
+ public int getRadius() {
+ return (int) (radius * scale);
+ }
+
+ /**
+ * Point intersection test.
+ */
+ boolean intersects(float x, float y) {
+ float dx = x - pos.x;
+ float dy = y - pos.y;
+
+ return dx * dx + dy * dy < getRadius() * getRadius();
+ }
+
+ /**
+ * Other circle intersection test.
+ */
+ boolean intersects(float x, float y, float radius) {
+ float dx = x - pos.x, dy = y - pos.y;
+ float tot_radius = getRadius() + radius;
+ return dx * dx + dy * dy < tot_radius * tot_radius;
+ }
+
+ public boolean isOnBorder(float w, float h) {
+ return (bounds.left < 0 || bounds.right > w || bounds.top < 0 || bounds.bottom > h);
+ }
+
+ /**
+ * Always return the normal radius of the bubble
+ *
+ * @return
+ */
+ public float getRetractedRadius() {
+ return radius;
+ }
+
+ public abstract boolean getHoldStatus();
+
+ public abstract boolean getRecordStatus();
+
+ public abstract String getName();
+
+ public abstract boolean callIDEquals(String call);
+
+ public abstract String getCallID();
+
+ public boolean isConference() {
+ return false;
+ }
+
+
+}
diff --git a/ring-android/src/cx/ring/model/BubbleContact.java b/ring-android/src/cx/ring/model/BubbleContact.java
new file mode 100644
index 0000000..e21f98a
--- /dev/null
+++ b/ring-android/src/cx/ring/model/BubbleContact.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model;
+
+import android.content.Context;
+
+public class BubbleContact extends Bubble {
+
+ public SipCall associated_call;
+
+ public BubbleContact(Context context, SipCall call, float x, float y, float size) {
+ super(context, call.getmContact(), x, y, size);
+ associated_call = call;
+ }
+
+ @Override
+ public boolean getHoldStatus() {
+ return associated_call.isOnHold();
+ }
+
+ @Override
+ public boolean getRecordStatus() {
+ return associated_call.isRecording();
+ }
+
+ public SipCall getCall() {
+ return associated_call;
+ }
+
+ public void setCall(SipCall call) {
+ associated_call = call;
+ }
+
+ @Override
+ public String getName() {
+ return associated_call.getmContact().getmDisplayName();
+ }
+
+ @Override
+ public boolean callIDEquals(String call) {
+ return associated_call.getCallId().contentEquals(call);
+ }
+
+ @Override
+ public String getCallID() {
+ return associated_call.getCallId();
+ }
+
+}
diff --git a/ring-android/src/cx/ring/model/BubbleModel.java b/ring-android/src/cx/ring/model/BubbleModel.java
new file mode 100644
index 0000000..221ff2e
--- /dev/null
+++ b/ring-android/src/cx/ring/model/BubbleModel.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Adrien Beraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model;
+
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BubbleModel {
+ private static final String TAG = BubbleModel.class.getSimpleName();
+
+ public enum State {
+ None, Incoming, Outgoing, Incall
+ }
+
+ public interface ModelCallback {
+ public void bubbleGrabbed(Bubble b);
+
+ /**
+ * A bubble is put beyond view borders.
+ * @param b The bubble
+ * @return true if the bubble should be ejected, false if it should bounce.
+ */
+ public boolean bubbleEjected(Bubble b);
+ }
+
+ public interface ActionGroupCallback {
+
+ /**
+ * Called when a bubble is on the "active" zone of the attractor.
+ *
+ * @param b The bubble that is on the attractor.
+ * @return true if the bubble should be removed from the model, false otherwise.
+ */
+ public boolean onBubbleAction(Bubble b, int action);
+ }
+
+ static public class ActionGroup {
+ public boolean enabled = true;
+ private final ArrayList<Attractor> buttons = new ArrayList<Attractor>();
+ private final ActionGroupCallback callback;
+ final private float margin;
+ public Bubble bubble = null;
+ public long viewStart = 0;
+ private final float appearTime;
+ private final float disappearTime;
+
+ public ActionGroup(ActionGroupCallback cb, float btn_margin, float appear_time, float disapear_time) {
+ this.callback = cb;
+ this.margin = btn_margin;
+ appearTime = appear_time;
+ disappearTime = disapear_time;
+ }
+
+ public ArrayList<Attractor> getActions() {
+ return buttons;
+ }
+
+ public void addAction(final int id, Bitmap btn, String name, float size) {
+ final Attractor a = new Attractor(name, size, new Attractor.Callback() {
+ @Override
+ public boolean onBubbleSucked(Bubble b) {
+ if (!enabled) return false;
+ return callback.onBubbleAction(b, id);
+ }
+ }, btn);
+ buttons.add(a);
+ }
+
+ public void show(Bubble b) {
+ this.bubble = b;
+ long now = System.nanoTime();
+ double dt = (now - viewStart) / 1000000000.;
+ double r = 1. - Math.min(dt / disappearTime, 1.);
+ this.enabled = true;
+ this.viewStart = now - (long)(r * appearTime * 1000000000.);
+ }
+
+ public void hide() {
+ long now = System.nanoTime();
+ double dt = (now - viewStart) / 1000000000.;
+ double r = 1. - Math.min(dt / appearTime, 1.);
+ this.enabled = false;
+ viewStart = now - (long)(r * disappearTime * 1000000000.);
+ }
+
+ public float getVisibility(long now) {
+ double dt = (now - viewStart) / 1000000000.;
+ if (enabled)
+ return (float) Math.min(dt / appearTime, 1.);
+ else
+ return 1.f - (float) Math.min(dt / disappearTime, 1.);
+ }
+
+ public void order(int w, int h) {
+ int n = buttons.size();
+ if (n == 0) return;
+ //float y = h - 3 * buttons.get(0).radius;
+ float y = bubble.getPosY() - margin - bubble.radius;
+ final float WIDTH = 2 * buttons.get(0).radius;
+ float totw = n * WIDTH + (n-1) * margin;
+ float xs = (w - totw) / 2 + buttons.get(0).radius;
+ float xstep = WIDTH+margin;
+ for (int i=0; i<n; i++) {
+ buttons.get(i).setPos(xs + i*xstep, y);
+ }
+ }
+ }
+
+ private final ModelCallback callback;
+
+ private long lastUpdate = 0;
+ private int width, height;
+ private final ArrayList<Bubble> bubbles = new ArrayList<Bubble>();
+ private final ArrayList<Attractor> attractors = new ArrayList<Attractor>();
+ private ActionGroup actions = null;
+
+ private static final double BUBBLE_RETURN_TIME_HALF_LIFE = .3;
+ private static final double BUBBLE_RETURN_TIME_LAMBDA = Math.log(2) / BUBBLE_RETURN_TIME_HALF_LIFE;
+
+ private static final double FRICTION_VISCOUS = Math.log(2) / .2f; // Viscous friction factor
+
+ private static final float BUBBLE_MAX_SPEED = 2500.f; // px.s⁻¹ : Max target speed in px/sec
+ private static final float ATTRACTOR_SMOOTH_DIST = 50.f; // px : Size of the "gravity hole" around the attractor
+ private static final float ATTRACTOR_STALL_DIST = 15.f; // px : Size of the "gravity hole" flat bottom
+ private static final float ATTRACTOR_DIST_SUCK = 20.f; // px
+
+ private static final float BORDER_REPULSION = 60000; // px.s⁻²
+
+ private final float border_repulsion;
+ private final float bubble_max_speed;
+ private final float attractor_smooth_dist;
+ private final float attractor_stall_dist;
+ private final float attractor_dist_suck;
+
+ private final float density;
+
+ private float circle_radius;
+ private final PointF circle_center = new PointF();
+
+ public State curState = State.None;
+
+ public BubbleModel(float screen_density, ModelCallback cb) {
+ Log.d(TAG, "Creating BubbleModel");
+ callback = cb;
+ this.density = screen_density;
+ attractor_dist_suck = ATTRACTOR_DIST_SUCK * density;
+ bubble_max_speed = BUBBLE_MAX_SPEED * density;
+ attractor_smooth_dist = ATTRACTOR_SMOOTH_DIST * density;
+ attractor_stall_dist = ATTRACTOR_STALL_DIST * density;
+ border_repulsion = BORDER_REPULSION * density;
+ }
+
+ public void setSize(int w, int h, float bubble_sz)
+ {
+ width = w;
+ height = h;
+ for (Attractor a : attractors) {
+ a.setSize(w, h);
+ }
+ if (actions != null) {
+ actions.order(width, height);
+ }
+ circle_radius = Math.min(width, height) / 2 - bubble_sz;
+ circle_center.set(width/2, height/2);
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public float getCircleSize() {
+ return circle_radius;
+ }
+
+ public PointF getCircleCenter() {
+ return circle_center;
+ }
+
+ public void addBubble(Bubble b) {
+ bubbles.add(b);
+ }
+
+ public List<Bubble> getBubbles() {
+ return bubbles;
+ }
+
+ public Bubble getBubble(String call) {
+ for (Bubble b : bubbles) {
+ if (!b.isUser && b.callIDEquals(call))
+ return b;
+ }
+ return null;
+ }
+
+ public void removeBubble(SipCall sipCall) {
+ bubbles.remove(getBubble(sipCall.getCallId()));
+
+ }
+
+ public void addAttractor(Attractor a) {
+ attractors.add(a);
+ }
+
+ public List<Attractor> getAttractors() {
+ return attractors;
+ }
+
+ public void clearAttractors() {
+ attractors.clear();
+ }
+
+ public void setActions(Bubble b, ActionGroup actions) {
+ actions.show(b);
+ /*actions.enabled = true;
+ actions.viewStart = 0;*/
+ actions.order(width, height);
+ this.actions = actions;
+ }
+
+ public ActionGroup getActions() {
+ return actions;
+ }
+
+ public void clearActions() {
+ this.actions = null;
+ }
+
+ public Bubble getUser() {
+ for (Bubble b : bubbles) {
+ if (b.isUser)
+ return b;
+ }
+ return null;
+ }
+
+ public void clear() {
+ clearAttractors();
+ bubbles.clear();
+ }
+
+ public void grabBubble(Bubble b) {
+ b.grab();
+ callback.bubbleGrabbed(b);
+ }
+
+ public void ungrabBubble(Bubble b) {
+ b.ungrab();
+ }
+
+ public void ejectBubble(Bubble b) {
+ boolean eject = callback.bubbleEjected(b);
+ if (eject) {
+ b.close();
+ }
+ }
+
+ public void update() {
+ /* INFO: if you get a NullPointer or OutOfBounds exception here,
+ * you may have some wrong/missing locks. */
+
+ long now = System.nanoTime();
+
+ // Do nothing if lastUpdate is in the future.
+ if (lastUpdate > now)
+ return;
+
+ double ddt = Math.min((now - lastUpdate) / 1000000000.0, .2);
+ lastUpdate = now;
+
+ float dt = (float) ddt;
+
+ //int attr_n = attractors.size();
+ boolean actionAttr = false;
+
+ // Iterators should not be used in frequently called methods
+ // to avoid garbage collection glitches caused by iterator objects.
+ for (int i = 0, n = bubbles.size(); i < n; i++) {
+ Bubble b = bubbles.get(i);
+
+ if (b.markedToDie) {
+ b.update(dt);
+ continue;
+ }
+
+ float bx = b.getPosX(), by = b.getPosY();
+
+ Attractor attractor = null;
+ PointF attractor_pos = b.attractionPoint;
+ float attractor_dist = (attractor_pos.x - bx) * (attractor_pos.x - bx) + (attractor_pos.y - by) * (attractor_pos.y - by);
+
+ boolean actionGrp = actions != null && actions.enabled && actions.bubble == b;
+ final List<Attractor> attr = (actionGrp) ? actions.getActions() : attractors;
+
+ for (Attractor t : attr) {
+ float dx = t.pos.x - bx, dy = t.pos.y - by;
+ float adist = dx * dx + dy * dy;
+ if (adist < attractor_dist) {
+ attractor = t;
+ attractor_pos = t.pos;
+ attractor_dist = adist;
+ }
+ }
+
+ b.attractor = attractor;
+
+ if (!b.isGrabbed()) {
+ if (actionGrp) {
+ for (Attractor anAttr : attr) {
+ if (anAttr == attractor) {
+ actionAttr = true;
+ break;
+ }
+ }
+ }
+
+ // float friction_coef = 1.f-FRICTION_VISCOUS*dt;
+ double friction_coef = 1 + Math.expm1(-FRICTION_VISCOUS * ddt);
+ b.speed.x *= friction_coef;
+ b.speed.y *= friction_coef;
+
+ float target_speed;
+ float tdx = attractor_pos.x - bx, tdy = attractor_pos.y - by;
+ float dist = Math.max(1.f, (float) Math.sqrt(tdx * tdx + tdy * tdy));
+ if (dist > attractor_smooth_dist)
+ target_speed = bubble_max_speed;
+ else if (dist < attractor_stall_dist)
+ target_speed = 0;
+ else {
+ float a = (dist - attractor_stall_dist) / (attractor_smooth_dist - attractor_stall_dist);
+ target_speed = bubble_max_speed * a;
+ }
+ if (attractor != null) {
+ if (dist > attractor_smooth_dist)
+ b.setTargetScale(1.f);
+ else if (dist < attractor_stall_dist)
+ b.setTargetScale(2f);
+ else {
+ float a = (dist - attractor_stall_dist) / (attractor_smooth_dist - attractor_stall_dist);
+ b.setTargetScale(a * .8f + .2f);
+ }
+ }
+
+ // border repulsion
+
+ if (bx < 0 && b.speed.x < 0) {
+ b.speed.x += dt * border_repulsion;
+ } else if (bx > width && b.speed.x > 0) {
+ b.speed.x -= dt * border_repulsion;
+ }
+ if (by < 0 && b.speed.y < 0) {
+ b.speed.y += dt * border_repulsion;
+ } else if (by > height && b.speed.y > 0) {
+ b.speed.y -= dt * border_repulsion;
+ }
+
+
+ b.speed.x += dt * target_speed * tdx / dist;
+ b.speed.y += dt * target_speed * tdy / dist;
+
+ double edt = -Math.expm1(-BUBBLE_RETURN_TIME_LAMBDA * ddt);
+ double dx = (attractor_pos.x - bx) * edt + Math.min(bubble_max_speed, b.speed.x) * dt;
+ double dy = (attractor_pos.y - by) * edt + Math.min(bubble_max_speed, b.speed.y) * dt;
+ // Log.w(TAG, "update dx="+dt+" dy="+dy);
+ b.setPos((float) (bx + dx), (float) (by + dy));
+
+ /*if (b.isOnBorder(width, height)) {
+ ejectBubble(b);
+ }*/
+
+ if (attractor != null && attractor_dist < attractor_dist_suck * attractor_dist_suck) {
+ boolean removeBubble = attractor.callback.onBubbleSucked(b);
+ if (removeBubble) {
+ bubbles.remove(b);
+ n--;
+ } else {
+ b.setTargetScale(1.f);
+ }
+
+ if (actionGrp) {
+ actions.hide();
+ }
+ }
+ } else {
+ actionAttr = true;
+ }
+
+ b.update(dt);
+ }
+
+ if (actions != null && actions.enabled && !actionAttr) {
+ actions.hide();
+ }
+ }
+
+}
diff --git a/ring-android/src/cx/ring/model/BubbleUser.java b/ring-android/src/cx/ring/model/BubbleUser.java
new file mode 100644
index 0000000..d79e5c2
--- /dev/null
+++ b/ring-android/src/cx/ring/model/BubbleUser.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model;
+
+import android.content.Context;
+import cx.ring.R;
+
+public class BubbleUser extends Bubble {
+
+ public Conference associated_call;
+
+ public BubbleUser(Context context, CallContact m, Conference conf, float x, float y, float size) {
+ super(context, m, x, y, size);
+ isUser = true;
+ associated_call = conf;
+ }
+
+ @Override
+ public boolean getHoldStatus() {
+ return associated_call.isOnHold();
+ }
+
+ @Override
+ public boolean getRecordStatus() {
+ return associated_call.isRecording();
+ }
+
+ public Conference getConference() {
+ return associated_call;
+ }
+
+ public void setConference(Conference c) {
+ associated_call = c;
+ }
+
+ @Override
+ public String getName() {
+ return mContext.getResources().getString(R.string.me);
+ }
+
+ @Override
+ public boolean callIDEquals(String call) {
+ return associated_call.getId().contentEquals(call);
+ }
+
+ @Override
+ public String getCallID() {
+ if (associated_call.hasMultipleParticipants())
+ return associated_call.getId();
+ else
+ return associated_call.getParticipants().get(0).getCallId();
+ }
+
+ @Override
+ public boolean isConference() {
+ return associated_call.hasMultipleParticipants();
+ }
+
+}
diff --git a/ring-android/src/cx/ring/model/BubblesView.java b/ring-android/src/cx/ring/model/BubblesView.java
new file mode 100644
index 0000000..8b465dd
--- /dev/null
+++ b/ring-android/src/cx/ring/model/BubblesView.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.*;
+import android.graphics.Paint.Align;
+import android.opengl.GLSurfaceView;
+import android.util.AttributeSet;
+import android.util.FloatMath;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.OvershootInterpolator;
+import cx.ring.R;
+
+import java.util.List;
+
+public class BubblesView extends GLSurfaceView implements SurfaceHolder.Callback, OnTouchListener {
+ private static final String TAG = BubblesView.class.getSimpleName();
+
+ private BubblesThread thread = null;
+ private BubbleModel model;
+
+ private Paint black_name_paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private Paint white_name_paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private Paint canvas_paint = new Paint();
+ private Paint circle_paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private Paint action_paint = new Paint();
+
+ static private final Interpolator interpolator = new OvershootInterpolator(2.f);
+ static private final Interpolator interpolator_dec = new DecelerateInterpolator();
+
+ private final Bitmap ic_bg;
+ private final Bitmap ic_bg_sel;
+
+ private GestureDetector gDetector;
+
+ //private float density;
+ private float textDensity;
+ private float bubbleActionTextDistMin;
+ private float bubbleActionTextDistMax;
+
+ private boolean dragging_bubble = false;
+
+ public BubblesView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ final Resources r = getResources();
+ //density = r.getDisplayMetrics().density;
+ textDensity = r.getDisplayMetrics().scaledDensity;
+ bubbleActionTextDistMin = r.getDimension(R.dimen.bubble_action_textdistmin);
+ bubbleActionTextDistMax = r.getDimension(R.dimen.bubble_action_textdistmax);
+
+ ic_bg = BitmapFactory.decodeResource(r, R.drawable.ic_bg);
+ ic_bg_sel = BitmapFactory.decodeResource(r, R.drawable.ic_bg_sel);
+
+ if (isInEditMode()) return;
+
+ SurfaceHolder holder = getHolder();
+ holder.addCallback(this);
+
+ this.setZOrderOnTop(true); // necessary
+ holder.setFormat(PixelFormat.TRANSLUCENT);
+ // create thread only; it's started in surfaceCreated()
+ createThread();
+
+ setOnTouchListener(this);
+ setFocusable(true);
+
+ black_name_paint.setTextSize(18 * textDensity);
+ black_name_paint.setColor(0xFF303030);
+ black_name_paint.setTextAlign(Align.CENTER);
+
+ white_name_paint.setTextSize(18 * textDensity);
+ white_name_paint.setColor(0xFFEEEEEE);
+ white_name_paint.setTextAlign(Align.CENTER);
+
+ circle_paint.setStyle(Paint.Style.STROKE);
+ circle_paint.setColor(r.getColor(R.color.darker_gray));
+ circle_paint.setXfermode(null);
+
+ gDetector = new GestureDetector(getContext(), new BubbleGestureListener());
+ gDetector.setIsLongpressEnabled(false);
+ }
+
+ private void createThread() {
+ if (thread != null)
+ return;
+ thread = new BubblesThread(getHolder(), getContext());
+ if (model != null)
+ thread.setModel(model);
+ }
+
+ public void setModel(BubbleModel model) {
+ this.model = model;
+ thread.setModel(model);
+ }
+
+ /*
+ * @Override public void onWindowFocusChanged(boolean hasWindowFocus) { if (!hasWindowFocus) { thread.pause(); } }
+ */
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ Log.w(TAG, "surfaceChanged " + width + "-" + height);
+ /*if (height < model.getHeight()) // probably showing the keyboard, don't move!
+ return;
+
+ thread.setSurfaceSize(width, height);*/
+ }
+
+ /*
+ * Callback invoked when the Surface has been created and is ready to be used.
+ */
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ // start the thread here so that we don't busy-wait in run()
+ // waiting for the surface to be created
+ createThread();
+
+ Log.w(TAG, "surfaceCreated");
+ thread.setRunning(true);
+ thread.start();
+ }
+
+ /*
+ * Callback invoked when the Surface has been destroyed and must no longer be touched. WARNING: after this method returns, the Surface/Canvas must
+ * never be touched again!
+ */
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ // we have to tell thread to shut down & wait for it to finish, or else
+ // it might touch the Surface after we return and explode
+ Log.w(TAG, "surfaceDestroyed");
+ boolean retry = true;
+ thread.setRunning(false);
+ thread.setPaused(false);
+ while (retry) {
+ try {
+ Log.w(TAG, "joining...");
+ thread.join();
+ retry = false;
+ } catch (InterruptedException ignored) {
+ }
+ }
+ Log.w(TAG, "done");
+ thread = null;
+ }
+
+ public boolean isDraggingBubble() {
+ return dragging_bubble;
+ }
+
+ class BubblesThread extends Thread {
+ private boolean running = false;
+ public boolean suspendFlag = false;
+ private SurfaceHolder surfaceHolder;
+
+ BubbleModel model = null;
+
+ public BubblesThread(SurfaceHolder holder, Context context) {
+ surfaceHolder = holder;
+ }
+
+ public void setModel(BubbleModel model) {
+ this.model = model;
+ }
+
+ @Override
+ public void run() {
+ while (running) {
+ Canvas c = null;
+ try {
+
+ if (suspendFlag) {
+ synchronized (this) {
+ while (suspendFlag) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ }
+ } else {
+ c = surfaceHolder.lockCanvas(null);
+
+ // for the case the surface is destroyed while already in the loop
+ if (c == null || model == null)
+ continue;
+
+ synchronized (model) {
+ model.update();
+ }
+ synchronized (surfaceHolder) {
+ // Log.w(TAG, "Thread doDraw");
+ synchronized (model) {
+ doDraw(c);
+ }
+ }
+ }
+
+ } finally {
+ if (c != null)
+ surfaceHolder.unlockCanvasAndPost(c);
+ }
+ }
+ }
+
+ public void setPaused(boolean wantToPause) {
+ synchronized (this) {
+ suspendFlag = wantToPause;
+ notify();
+ }
+ }
+
+ public void setRunning(boolean b) {
+ running = b;
+ }
+
+ /**
+ * got multiple IndexOutOfBoundsException, when switching calls. //FIXME
+ *
+ * @param canvas
+ */
+ private void doDraw(Canvas canvas) {
+ List<Bubble> bubbles = model.getBubbles();
+ List<Attractor> attractors = model.getAttractors();
+ BubbleModel.ActionGroup actions = model.getActions();
+
+ long now = System.nanoTime();
+
+ canvas_paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+ canvas.drawPaint(canvas_paint);
+ canvas_paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+
+ if (model.curState == BubbleModel.State.Incall || model.curState == BubbleModel.State.Outgoing) {
+ PointF center = model.getCircleCenter();
+ canvas.drawCircle(center.x, center.y, model.getCircleSize(), circle_paint);
+ }
+
+ for (Attractor a : attractors) {
+ canvas.drawBitmap(a.getBitmap(), null, a.getBounds(), null);
+ }
+
+ Bubble drawLater = (actions == null) ? null : actions.bubble;
+ for (Bubble b : bubbles) {
+ if (b == drawLater) continue;
+ canvas.drawBitmap(b.getBitmap(), null, b.getBounds(), null);
+ canvas.drawText(b.getName(), b.getPosX(), b.getPosY() - b.getRetractedRadius() * 1.2f, getNamePaint(b));
+ }
+
+ if (actions != null) {
+ float t = actions.getVisibility(now);
+ if (!actions.enabled && t == .0f) {
+ model.clearActions();
+ }
+ float showed = interpolator.getInterpolation(t);
+ float dark = interpolator_dec.getInterpolation(t);
+ float dist_range = bubbleActionTextDistMax - bubbleActionTextDistMin;
+ action_paint.setAlpha((int) (255 * t));
+
+ List<Attractor> acts = actions.getActions();
+ Bubble b = actions.bubble;
+
+ canvas.drawARGB((int)(dark*128), 0, 0, 0);
+
+ white_name_paint.setTextSize(18 * textDensity);
+ boolean suck_bubble = false;
+ for (Attractor a : acts) {
+ if (b.attractor == a) {
+ canvas.drawBitmap(ic_bg_sel, null, a.getBounds(showed * 2.f, b.getPos(), showed), action_paint);
+ suck_bubble = true;
+ } else
+ canvas.drawBitmap(ic_bg, null, a.getBounds(showed * 2.f, b.getPos(), showed), action_paint);
+ canvas.drawBitmap(a.getBitmap(), null, a.getBounds(showed, b.getPos(), showed), null);
+ float dist_raw = FloatMath.sqrt((b.pos.x - a.pos.x) * (b.pos.x - a.pos.x) + (b.pos.y - a.pos.y) * (b.pos.y - a.pos.y));
+ float dist_min = a.radius + b.radius + bubbleActionTextDistMin;
+ float dist = Math.max(0, dist_raw - dist_min);
+ if (actions.enabled && dist < dist_range) {
+ white_name_paint.setAlpha(255 - (int) (255 * dist / dist_range));
+ canvas.drawText(a.name, a.getBounds().centerX(), a.getBounds().centerY() - a.radius * 2.2f, white_name_paint);
+ }
+ }
+ white_name_paint.setAlpha(255);
+
+ canvas.drawBitmap(drawLater.getBitmap(), null, drawLater.getBounds(), (!actions.enabled && suck_bubble)? action_paint : null);
+ }
+ }
+ }
+
+ private Paint getNamePaint(Bubble b) {
+ black_name_paint.setTextSize(18/* * b.targetScale */ * textDensity);
+ return black_name_paint;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ // Log.w(TAG, "onTouch " + event.getAction());
+
+ int action = event.getActionMasked();
+
+ if (gDetector.onTouchEvent(event))
+ return true;
+
+ if (action == MotionEvent.ACTION_UP) {
+ if (thread.suspendFlag) {
+ Log.i(TAG, "Relaunch drawing thread");
+ thread.setPaused(false);
+ }
+ List<Bubble> bubbles = model.getBubbles();
+ for (Bubble b : bubbles) {
+ if (b.isGrabbed()) {
+ model.ungrabBubble(b);
+ }
+ }
+ dragging_bubble = false;
+ } else if (action != MotionEvent.ACTION_DOWN && !isDraggingBubble() && !thread.suspendFlag) {
+ Log.i(TAG, "Not dragging thread should be stopped");
+ thread.setPaused(true);
+ }
+ return true;
+ }
+
+ public void restartDrawing() {
+ if (thread != null && thread.suspendFlag) {
+ Log.i(TAG, "Relaunch drawing thread");
+ thread.setPaused(false);
+ }
+ }
+
+ public void stopThread() {
+ if (thread != null && thread.suspendFlag) {
+ Log.i(TAG, "Stop drawing thread");
+ thread.setPaused(true);
+ }
+ }
+
+ class BubbleGestureListener implements OnGestureListener {
+ @Override
+ public boolean onDown(MotionEvent event) {
+ synchronized (model) {
+ List<Bubble> bubbles = model.getBubbles();
+ for (Bubble b : bubbles) {
+ if (b.intersects(event.getX(), event.getY())) {
+ model.grabBubble(b);
+ b.setPos(event.getX(), event.getY());
+ dragging_bubble = true;
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return false;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent event, float distanceX, float distanceY) {
+ synchronized (model) {
+ List<Bubble> bubbles = model.getBubbles();
+ for (Bubble b : bubbles) {
+ if (b.isGrabbed()) {
+ b.drag(event.getX(), event.getY());
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return false;
+ }
+ }
+}
diff --git a/ring-android/src/cx/ring/model/CallContact.java b/ring-android/src/cx/ring/model/CallContact.java
new file mode 100644
index 0000000..c067a75
--- /dev/null
+++ b/ring-android/src/cx/ring/model/CallContact.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.model;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Profile;
+
+public class CallContact implements Parcelable {
+
+ public static int DEFAULT_ID = 0;
+
+ private long id;
+ private String mDisplayName;
+ private long photo_id;
+ private ArrayList<Phone> phones, sip_phones;
+ private String mEmail;
+ private boolean isUser;
+ private WeakReference<Bitmap> contact_photo = new WeakReference<Bitmap>(null);
+
+ private CallContact(long cID, String displayName, long photoID, ArrayList<Phone> p, ArrayList<Phone> sip, String mail, boolean user) {
+ id = cID;
+ mDisplayName = displayName;
+ phones = p;
+ sip_phones = sip;
+ mEmail = mail;
+ photo_id = photoID;
+ isUser = user;
+ }
+
+ public CallContact(Parcel in) {
+ readFromParcel(in);
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public String getmDisplayName() {
+ return mDisplayName;
+ }
+
+ public long getPhoto_id() {
+ return photo_id;
+ }
+
+ public void setPhoto_id(long photo_id) {
+ this.photo_id = photo_id;
+ }
+
+ public ArrayList<Phone> getPhones() {
+ return phones;
+ }
+
+ public void setPhones(ArrayList<Phone> phones) {
+ this.phones = phones;
+ }
+
+ public ArrayList<Phone> getSip_phones() {
+ return sip_phones;
+ }
+
+ public void setSip_phones(ArrayList<Phone> sip_phones) {
+ this.sip_phones = sip_phones;
+ }
+
+ public Phone getSipPhone() {
+ if (sip_phones.size() > 0) {
+ return sip_phones.get(0);
+ }
+ if (phones.size() > 0) {
+ return phones.get(0);
+ }
+ return null;
+ }
+
+ public String getmEmail() {
+ return mEmail;
+ }
+
+ public void setmEmail(String mEmail) {
+ this.mEmail = mEmail;
+ }
+
+ @Override
+ public String toString() {
+ return mDisplayName;
+ }
+
+ public static class ContactBuilder {
+
+ long contactID;
+ String contactName;
+ long contactPhoto;
+ ArrayList<Phone> phones;
+ ArrayList<Phone> sip;
+ String contactMail;
+
+ public ContactBuilder startNewContact(long id, String displayName, long photo_id) {
+ contactID = id;
+
+ contactName = displayName;
+ contactPhoto = photo_id;
+ phones = new ArrayList<Phone>();
+ sip = new ArrayList<Phone>();
+ return this;
+ }
+
+ public ContactBuilder addPhoneNumber(String num, int type) {
+ phones.add(new Phone(num, type));
+ return this;
+ }
+
+ public ContactBuilder addSipNumber(String num, int type) {
+ sip.add(new Phone(num, type));
+ return this;
+ }
+
+ public CallContact build() {
+ return new CallContact(contactID, contactName, contactPhoto, phones, sip, contactMail, false);
+ }
+
+ public static ContactBuilder getInstance() {
+ return new ContactBuilder();
+ }
+
+ public static CallContact buildUnknownContact(String to) {
+ ArrayList<Phone> phones = new ArrayList<Phone>();
+ phones.add(new Phone(to, 0));
+
+ return new CallContact(-1, to, 0, phones, new ArrayList<CallContact.Phone>(), "", false);
+ }
+
+ public static CallContact buildUserContact(ContentResolver cr) {
+ String[] mProjection = new String[] { Profile._ID, Profile.DISPLAY_NAME_PRIMARY, Profile.PHOTO_ID };
+ Cursor mProfileCursor = cr.query(Profile.CONTENT_URI, mProjection, null, null, null);
+ CallContact result;
+ if (mProfileCursor.getCount() > 0) {
+ mProfileCursor.moveToFirst();
+ String displayName = mProfileCursor.getString(mProfileCursor.getColumnIndex(Profile.DISPLAY_NAME_PRIMARY));
+
+ result = new CallContact(mProfileCursor.getLong(mProfileCursor.getColumnIndex(Profile._ID)), displayName,
+ mProfileCursor.getLong(mProfileCursor.getColumnIndex(Profile.PHOTO_ID)), new ArrayList<Phone>(),
+ new ArrayList<CallContact.Phone>(), "", true);
+ } else {
+ result = new CallContact(-1, "Me", 0, new ArrayList<Phone>(), new ArrayList<CallContact.Phone>(), "", true);
+ }
+ mProfileCursor.close();
+ return result;
+ }
+
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(id);
+ dest.writeString(mDisplayName);
+ dest.writeLong(photo_id);
+ dest.writeTypedList(phones);
+
+ dest.writeTypedList(sip_phones);
+
+ dest.writeString(mEmail);
+ dest.writeByte((byte) (isUser ? 1 : 0));
+
+ }
+
+ private void readFromParcel(Parcel in) {
+
+ id = in.readLong();
+ mDisplayName = in.readString();
+ photo_id = in.readLong();
+ phones = new ArrayList<CallContact.Phone>();
+ sip_phones = new ArrayList<CallContact.Phone>();
+ in.readTypedList(phones, Phone.CREATOR);
+ in.readTypedList(sip_phones, Phone.CREATOR);
+ mEmail = in.readString();
+ isUser = in.readByte() == 1;
+ }
+
+ public static final Parcelable.Creator<CallContact> CREATOR = new Parcelable.Creator<CallContact>() {
+ @Override
+ public CallContact createFromParcel(Parcel in) {
+ return new CallContact(in);
+ }
+
+ @Override
+ public CallContact[] newArray(int size) {
+ return new CallContact[size];
+ }
+ };
+
+ public static class Phone implements Parcelable {
+
+ int type;
+ String number;
+
+ public Phone(String num, int ty) {
+ type = ty;
+ number = num;
+ }
+
+ public Phone(Parcel in) {
+ readFromParcel(in);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int arg1) {
+ dest.writeInt(type);
+ dest.writeString(number);
+ }
+
+ private void readFromParcel(Parcel in) {
+ type = in.readInt();
+ number = in.readString();
+ }
+
+ public static final Parcelable.Creator<Phone> CREATOR = new Parcelable.Creator<Phone>() {
+ @Override
+ public Phone createFromParcel(Parcel in) {
+ return new Phone(in);
+ }
+
+ @Override
+ public Phone[] newArray(int size) {
+ return new Phone[size];
+ }
+ };
+
+ public int getType() {
+ return type;
+ }
+
+ public void setType(int type) {
+ this.type = type;
+ }
+
+ public String getNumber() {
+ return number;
+ }
+
+ public void setNumber(String number) {
+ this.number = number;
+ }
+
+ }
+
+ public void addPhoneNumber(String tel, int type) {
+ phones.add(new Phone(tel, type));
+
+ }
+
+ public void addSipNumber(String tel, int type) {
+ sip_phones.add(new Phone(tel, type));
+
+ }
+
+ public boolean isUser() {
+ return isUser;
+ }
+
+ public boolean hasPhoto() {
+ if (contact_photo.get() != null)
+ return true;
+ return false;
+ }
+
+ public Bitmap getPhoto() {
+ return contact_photo.get();
+ }
+
+ public void setPhoto(Bitmap externalBMP) {
+ contact_photo = new WeakReference<Bitmap>(externalBMP);
+ }
+
+ /**
+ * A contact is Unknown when his name == his phone number
+ * @return true when Name == Number
+ */
+ public boolean isUnknown() {
+ return mDisplayName.contentEquals(phones.get(0).getNumber());
+ }
+
+}
diff --git a/ring-android/src/cx/ring/model/Codec.java b/ring-android/src/cx/ring/model/Codec.java
new file mode 100644
index 0000000..0bef48f
--- /dev/null
+++ b/ring-android/src/cx/ring/model/Codec.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model;
+
+import cx.ring.service.StringVect;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class Codec implements Parcelable {
+ int payload;
+ String name;
+ String sampleRate;
+ String bitRate;
+ String channels;
+ boolean enabled;
+
+ public Codec(int i, StringVect audioCodecDetails, boolean b) {
+ payload = i;
+ name = audioCodecDetails.get(0);
+ sampleRate = audioCodecDetails.get(1);
+ bitRate = audioCodecDetails.get(2);
+ channels = audioCodecDetails.get(3);
+ enabled = b;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(payload);
+ out.writeString(name);
+ out.writeString(sampleRate);
+ out.writeString(bitRate);
+ out.writeString(channels);
+ out.writeByte((byte) (enabled ? 1 : 0));
+ }
+
+ public static final Parcelable.Creator<Codec> CREATOR = new Parcelable.Creator<Codec>() {
+ public Codec createFromParcel(Parcel in) {
+ return new Codec(in);
+ }
+
+ public Codec[] newArray(int size) {
+ return new Codec[size];
+ }
+ };
+
+ private Codec(Parcel in) {
+ payload = in.readInt();
+ name = in.readString();
+ sampleRate = in.readString();
+ bitRate = in.readString();
+ channels = in.readString();
+ enabled = in.readByte() == 1;
+ }
+
+ public Codec(Codec c) {
+ payload = c.payload;
+ name = c.name;
+ sampleRate = c.sampleRate;
+ bitRate = c.bitRate;
+ channels = c.channels;
+ enabled = c.enabled;
+ }
+
+ @Override
+ public String toString() {
+ return "Codec: " + name + "\n" + "Payload: " + payload + "\n" + "Sample Rate: " + sampleRate + "\n" + "Bit Rate: " + bitRate + "\n"
+ + "Channels: " + channels;
+ }
+
+ public CharSequence getPayload() {
+ return Integer.toString(payload);
+ }
+
+ public CharSequence getName() {
+ return name;
+ }
+
+ public String getSampleRate() {
+ return sampleRate;
+ }
+
+ public String getBitRate() {
+ return bitRate;
+ }
+
+ public String getChannels() {
+ return channels;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean b) {
+ enabled = b;
+ }
+
+ public void toggleState() {
+ enabled = !enabled;
+
+ }
+
+ @Override
+ public boolean equals(Object o){
+ return o instanceof Codec && ((Codec) o).payload == payload;
+ }
+
+ public boolean isSpeex() {
+ return name.contentEquals("speex");
+ }
+
+}
diff --git a/ring-android/src/cx/ring/model/Conference.aidl b/ring-android/src/cx/ring/model/Conference.aidl
new file mode 100644
index 0000000..774e946
--- /dev/null
+++ b/ring-android/src/cx/ring/model/Conference.aidl
@@ -0,0 +1,4 @@
+package cx.ring.model;
+
+
+parcelable Conference;
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/model/Conference.java b/ring-android/src/cx/ring/model/Conference.java
new file mode 100644
index 0000000..db027f6
--- /dev/null
+++ b/ring-android/src/cx/ring/model/Conference.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model;
+
+import java.util.ArrayList;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class Conference implements Parcelable {
+
+ private String id;
+ private int mConfState;
+ private ArrayList<SipCall> participants;
+ private boolean recording;
+ private ArrayList<SipMessage> messages;
+
+ public static String DEFAULT_ID = "-1";
+
+ public boolean isRinging() {
+ return participants.get(0).isRinging();
+ }
+
+ public void removeParticipant(SipCall toRemove) {
+ participants.remove(toRemove);
+ }
+
+ public boolean useSecureLayer() {
+ for(SipCall call : participants){
+ if(call.getAccount().useSecureLayer())
+ return true;
+ }
+ return false;
+ }
+
+ public interface state {
+ int ACTIVE_ATTACHED = 0;
+ int ACTIVE_DETACHED = 1;
+ int ACTIVE_ATTACHED_REC = 2;
+ int ACTIVE_DETACHED_REC = 3;
+ int HOLD = 4;
+ int HOLD_REC = 5;
+ }
+
+ public void setCallState(String callID, int newState) {
+ if(id.contentEquals(callID))
+ mConfState = newState;
+ else {
+ getCallById(callID).setCallState(newState);
+ }
+ }
+
+ public void setCallState(String confID, String newState) {
+ if (newState.equals("ACTIVE_ATTACHED")) {
+ setCallState(confID, state.ACTIVE_ATTACHED);
+ } else if (newState.equals("ACTIVE_DETACHED")) {
+ setCallState(confID, state.ACTIVE_DETACHED);
+ } else if (newState.equals("ACTIVE_ATTACHED_REC")) {
+ setCallState(confID, state.ACTIVE_ATTACHED_REC);
+ } else if (newState.equals("ACTIVE_DETACHED_REC")) {
+ setCallState(confID, state.ACTIVE_DETACHED_REC);
+ } else if (newState.equals("HOLD")) {
+ setCallState(confID, state.HOLD);
+ } else if (newState.equals("HOLD_REC")) {
+ setCallState(confID, state.HOLD_REC);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(id);
+ out.writeInt(mConfState);
+ ArrayList<SipCall> normal_calls = new ArrayList<SipCall>();
+ ArrayList<SecureSipCall> secure_calls = new ArrayList<SecureSipCall>();
+
+ for(SipCall part : participants){
+ if(part instanceof SecureSipCall)
+ secure_calls.add((SecureSipCall) part);
+ else
+ normal_calls.add(part);
+ }
+ out.writeTypedList(secure_calls);
+ out.writeTypedList(normal_calls);
+ out.writeByte((byte) (recording ? 1 : 0));
+ out.writeTypedList(messages);
+ }
+
+ public static final Parcelable.Creator<Conference> CREATOR = new Parcelable.Creator<Conference>() {
+ public Conference createFromParcel(Parcel in) {
+ return new Conference(in);
+ }
+
+ public Conference[] newArray(int size) {
+ return new Conference[size];
+ }
+ };
+
+
+ private Conference(Parcel in) {
+ participants = new ArrayList<SipCall>();
+ id = in.readString();
+ mConfState = in.readInt();
+ ArrayList<SecureSipCall> tmp = new ArrayList<SecureSipCall>();
+ in.readTypedList(tmp, SecureSipCall.CREATOR);
+ in.readTypedList(participants, SipCall.CREATOR);
+ participants.addAll(tmp);
+ recording = in.readByte() == 1;
+ messages = new ArrayList<SipMessage>();
+ in.readTypedList(messages, SipMessage.CREATOR);
+ }
+
+ public Conference(SipCall call) {
+ this(DEFAULT_ID);
+ participants.add(call);
+ }
+
+ public Conference(String cID) {
+ id = cID;
+ participants = new ArrayList<SipCall>();
+ recording = false;
+ messages = new ArrayList<SipMessage>();
+ }
+
+ public Conference(Conference c) {
+ id = c.id;
+ mConfState = c.mConfState;
+ participants = new ArrayList<SipCall>(c.participants);
+ recording = c.recording;
+ messages = new ArrayList<SipMessage>();
+ }
+
+ public String getId() {
+ if(hasMultipleParticipants())
+ return id;
+ else
+ return participants.get(0).getCallId();
+ }
+
+ public String getState() {
+ if (participants.size() == 1) {
+ return participants.get(0).getCallStateString();
+ }
+ return getConferenceStateString();
+ }
+
+ public String getConferenceStateString() {
+
+ String text_state;
+
+ switch (mConfState) {
+ case state.ACTIVE_ATTACHED:
+ text_state = "ACTIVE_ATTACHED";
+ break;
+ case state.ACTIVE_DETACHED:
+ text_state = "ACTIVE_DETACHED";
+ break;
+ case state.ACTIVE_ATTACHED_REC:
+ text_state = "ACTIVE_ATTACHED_REC";
+ break;
+ case state.ACTIVE_DETACHED_REC:
+ text_state = "ACTIVE_DETACHED_REC";
+ break;
+ case state.HOLD:
+ text_state = "HOLD";
+ break;
+ case state.HOLD_REC:
+ text_state = "HOLD_REC";
+ break;
+ default:
+ text_state = "NULL";
+ }
+
+ return text_state;
+ }
+
+ public ArrayList<SipCall> getParticipants() {
+ return participants;
+ }
+
+ public boolean contains(String callID) {
+ for (SipCall participant : participants) {
+ if (participant.getCallId().contentEquals(callID))
+ return true;
+ }
+ return false;
+ }
+
+ public SipCall getCallById(String callID) {
+ for (SipCall participant : participants) {
+ if (participant.getCallId().contentEquals(callID))
+ return participant;
+ }
+ return null;
+ }
+
+ /**
+ * Compare conferences based on confID/participants
+ */
+ @Override
+ public boolean equals(Object c) {
+ if (c instanceof Conference) {
+ if (((Conference) c).id.contentEquals(id) && !id.contentEquals("-1")) {
+ return true;
+ } else {
+ if (((Conference) c).id.contentEquals(id)) {
+ for (SipCall participant : participants) {
+ if (!((Conference) c).contains(participant.getCallId()))
+ return false;
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+
+ }
+
+ public boolean hasMultipleParticipants() {
+ return participants.size() > 1;
+ }
+
+ public boolean isOnHold() {
+ return participants.size() == 1 && participants.get(0).isOnHold() || getConferenceStateString().contentEquals("HOLD");
+ }
+
+ public boolean isIncoming() {
+ return participants.size() == 1 && participants.get(0).isIncoming();
+ }
+
+
+ public void setRecording(boolean b) {
+ recording = b;
+ }
+
+ public boolean isRecording() {
+ return recording;
+ }
+
+
+ public boolean isOnGoing() {
+ return participants.size() == 1 && participants.get(0).isOngoing() || participants.size() > 1;
+ }
+
+ public ArrayList<SipMessage> getMessages() {
+ return messages;
+ }
+
+ public void addSipMessage(SipMessage sipMessage) {
+ messages.add(sipMessage);
+ }
+
+ public void addParticipant(SipCall part) {
+ participants.add(part);
+ }
+
+}
diff --git a/ring-android/src/cx/ring/model/SecureSipCall.java b/ring-android/src/cx/ring/model/SecureSipCall.java
new file mode 100644
index 0000000..5f5e729
--- /dev/null
+++ b/ring-android/src/cx/ring/model/SecureSipCall.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import cx.ring.model.account.AccountDetailSrtp;
+
+
+public class SecureSipCall extends SipCall {
+
+ public interface SecureLayer {
+ int ZRTP_LAYER = 0;
+ int SDES_LAYER = 1;
+ }
+
+ public final static int DISPLAY_GREEN_LOCK = 0;
+ public final static int DISPLAY_RED_LOCK = 1;
+ public final static int DISPLAY_CONFIRM_SAS = 2;
+ public final static int DISPLAY_NONE = 3;
+
+ int mSecureLayerUsed;
+ ZrtpModule mZrtpModule;
+ SdesModule mSdesModule;
+
+ private boolean isInitialized;
+
+ public SecureSipCall(SipCall call) {
+ super(call);
+ isInitialized = false;
+ String keyExchange = getAccount().getSrtpDetails().getDetailString(AccountDetailSrtp.CONFIG_SRTP_KEY_EXCHANGE);
+ if (keyExchange.contentEquals("zrtp")) {
+ mSecureLayerUsed = SecureLayer.ZRTP_LAYER;
+ } else if (keyExchange.contentEquals("sdes")) {
+ mSecureLayerUsed = SecureLayer.SDES_LAYER;
+ }
+
+ mZrtpModule = new ZrtpModule();
+ mSdesModule = new SdesModule();
+ }
+
+ public void setSASConfirmed(boolean confirmedSAS) {
+ mZrtpModule.needSASConfirmation = !confirmedSAS;
+ }
+
+ public String getSAS() {
+ return mZrtpModule.SAS;
+ }
+
+ public void setSAS(String SAS) {
+ mZrtpModule.SAS = SAS;
+ }
+
+ public SecureSipCall(Parcel in) {
+ super(in);
+ isInitialized = in.readByte() == 1;
+ mSecureLayerUsed = in.readInt();
+ mSdesModule = new SdesModule(in);
+ mZrtpModule = new ZrtpModule(in);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeByte((byte) (isInitialized ? 1 : 0));
+ out.writeInt(mSecureLayerUsed);
+ mSdesModule.writeToParcel(out);
+ mZrtpModule.writeToParcel(out);
+ }
+
+ public static final Parcelable.Creator<SecureSipCall> CREATOR = new Parcelable.Creator<SecureSipCall>() {
+ public SecureSipCall createFromParcel(Parcel in) {
+ return new SecureSipCall(in);
+ }
+
+ public SecureSipCall[] newArray(int size) {
+ return new SecureSipCall[size];
+ }
+ };
+
+ public void sasConfirmedByZrtpLayer(int verified) {
+ // Not used
+ }
+
+ public void setZrtpSupport(boolean support) {
+ mZrtpModule.zrtpIsSupported = support;
+ if (!support)
+ mZrtpModule.needSASConfirmation = false;
+ }
+
+ public void setInitialized() {
+ isInitialized = true;
+ }
+
+ /*
+ * returns what state should be visible during call
+ */
+ public int displayModule() {
+ if (isInitialized) {
+ Log.i("SecureSIp", "needSASConfirmation" + mZrtpModule.needSASConfirmation);
+ if (mZrtpModule.needSASConfirmation) {
+ return DISPLAY_CONFIRM_SAS;
+ } else if (mZrtpModule.zrtpIsSupported || mSdesModule.sdesIsOn) {
+ return DISPLAY_GREEN_LOCK;
+ } else {
+ return DISPLAY_RED_LOCK;
+ }
+ }
+ return DISPLAY_NONE;
+ }
+
+ public void useSecureSDES(boolean use) {
+ mSdesModule.sdesIsOn = use;
+ mZrtpModule.needSASConfirmation = false;
+ }
+
+
+ private class ZrtpModule {
+ private String SAS;
+ private boolean needSASConfirmation;
+ private boolean zrtpIsSupported;
+
+ // static preferences of account
+ private final boolean displaySas;
+ private final boolean alertIfZrtpNotSupported;
+ private final boolean displaySASOnHold;
+
+ public ZrtpModule() {
+ displaySas = getAccount().getSrtpDetails().getDetailBoolean(AccountDetailSrtp.CONFIG_ZRTP_DISPLAY_SAS);
+ alertIfZrtpNotSupported = getAccount().getSrtpDetails().getDetailBoolean(AccountDetailSrtp.CONFIG_ZRTP_NOT_SUPP_WARNING);
+ displaySASOnHold = getAccount().getSrtpDetails().getDetailBoolean(AccountDetailSrtp.CONFIG_ZRTP_NOT_SUPP_WARNING);
+ needSASConfirmation = displaySas;
+ zrtpIsSupported = false;
+ }
+
+ public ZrtpModule(Parcel in) {
+ SAS = in.readString();
+ displaySas = in.readByte() == 1;
+ alertIfZrtpNotSupported = in.readByte() == 1;
+ displaySASOnHold = in.readByte() == 1;
+ zrtpIsSupported = in.readByte() == 1;
+ needSASConfirmation = in.readByte() == 1;
+ }
+
+ public void writeToParcel(Parcel dest) {
+ dest.writeString(SAS);
+ dest.writeByte((byte) (displaySas ? 1 : 0));
+ dest.writeByte((byte) (alertIfZrtpNotSupported ? 1 : 0));
+ dest.writeByte((byte) (displaySASOnHold ? 1 : 0));
+ dest.writeByte((byte) (zrtpIsSupported ? 1 : 0));
+ dest.writeByte((byte) (needSASConfirmation ? 1 : 0));
+ }
+ }
+
+ private class SdesModule {
+
+ private boolean sdesIsOn;
+
+ public SdesModule() {
+ sdesIsOn = false;
+ }
+
+ public SdesModule(Parcel in) {
+ sdesIsOn = in.readByte() == 1;
+ }
+
+ public void writeToParcel(Parcel dest) {
+ dest.writeByte((byte) (sdesIsOn ? 1 : 0));
+ }
+ }
+}
diff --git a/ring-android/src/cx/ring/model/SipCall.aidl b/ring-android/src/cx/ring/model/SipCall.aidl
new file mode 100644
index 0000000..13c290c
--- /dev/null
+++ b/ring-android/src/cx/ring/model/SipCall.aidl
@@ -0,0 +1,4 @@
+package cx.ring.model;
+
+
+parcelable SipCall;
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/model/SipCall.java b/ring-android/src/cx/ring/model/SipCall.java
new file mode 100644
index 0000000..f64a505
--- /dev/null
+++ b/ring-android/src/cx/ring/model/SipCall.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux>
+ * Alexandre Savard <alexandre.savard@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.model;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import cx.ring.model.account.Account;
+
+public class SipCall implements Parcelable {
+
+ public static String ID = "id";
+ public static String ACCOUNT = "account";
+ public static String CONTACT = "contact";
+ public static String TYPE = "type";
+ public static String STATE = "state";
+
+ private static final String TAG = SipCall.class.getSimpleName();
+
+ private String mCallID = "";
+ private Account mAccount = null;
+ private CallContact mContact = null;
+ private boolean isRecording = false;
+ private long timestampStart_ = 0;
+ private long timestampEnd_ = 0;
+
+ private int mCallType;
+ private int mCallState = state.CALL_STATE_NONE;
+
+ public SipCall(SipCall call) {
+ mCallID = call.mCallID;
+ mAccount = call.mAccount;
+ mContact = call.mContact;
+ isRecording = call.isRecording;
+ timestampStart_ = call.timestampStart_;
+ timestampEnd_ = call.timestampEnd_;
+ mCallType = call.mCallType;
+ mCallState = call.mCallState;
+ }
+
+ /**
+ * *********************
+ * Construtors
+ * *********************
+ */
+
+ protected SipCall(Parcel in) {
+
+ mCallID = in.readString();
+ mAccount = in.readParcelable(Account.class.getClassLoader());
+ mContact = in.readParcelable(CallContact.class.getClassLoader());
+ isRecording = in.readByte() == 1;
+ mCallType = in.readInt();
+ mCallState = in.readInt();
+ timestampStart_ = in.readLong();
+ timestampEnd_ = in.readLong();
+ }
+
+ public SipCall(Bundle args) {
+ mCallID = args.getString(ID);
+ mAccount = args.getParcelable(ACCOUNT);
+ mCallType = args.getInt(TYPE);
+ mCallState = args.getInt(STATE);
+ mContact = args.getParcelable(CONTACT);
+ }
+
+ public long getTimestampEnd_() {
+ return timestampEnd_;
+ }
+
+ public String getRecordPath() {
+ return "";
+ }
+
+ public int getCallType() {
+ return mCallType;
+ }
+
+ public Bundle getBundle() {
+ Bundle args = new Bundle();
+ args.putString(SipCall.ID, mCallID);
+ args.putParcelable(SipCall.ACCOUNT, mAccount);
+ args.putInt(SipCall.STATE, mCallState);
+ args.putInt(SipCall.TYPE, mCallType);
+ args.putParcelable(SipCall.CONTACT, mContact);
+ return args;
+ }
+
+
+ public interface direction {
+ public static final int CALL_TYPE_INCOMING = 1;
+ public static final int CALL_TYPE_OUTGOING = 2;
+ }
+
+ public interface state {
+ public static final int CALL_STATE_NONE = 0;
+ public static final int CALL_STATE_RINGING = 2;
+ public static final int CALL_STATE_CURRENT = 3;
+ public static final int CALL_STATE_HUNGUP = 4;
+ public static final int CALL_STATE_BUSY = 5;
+ public static final int CALL_STATE_FAILURE = 6;
+ public static final int CALL_STATE_HOLD = 7;
+ public static final int CALL_STATE_UNHOLD = 8;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+
+ out.writeString(mCallID);
+ out.writeParcelable(mAccount, 0);
+
+ out.writeParcelable(mContact, 0);
+ out.writeByte((byte) (isRecording ? 1 : 0));
+ out.writeInt(mCallType);
+ out.writeInt(mCallState);
+ out.writeLong(timestampStart_);
+ out.writeLong(timestampEnd_);
+ }
+
+ public static final Parcelable.Creator<SipCall> CREATOR = new Parcelable.Creator<SipCall>() {
+ public SipCall createFromParcel(Parcel in) {
+ return new SipCall(in);
+ }
+
+ public SipCall[] newArray(int size) {
+ return new SipCall[size];
+ }
+ };
+
+ public void setCallID(String callID) {
+ mCallID = callID;
+ }
+
+ public String getCallId() {
+ return mCallID;
+ }
+
+ public long getTimestampStart_() {
+ return timestampStart_;
+ }
+
+ public void setTimestampStart_(long timestampStart_) {
+ this.timestampStart_ = timestampStart_;
+ }
+
+ public void setTimestampEnd_(long timestampEnd_) {
+ this.timestampEnd_ = timestampEnd_;
+ }
+
+ public void setAccount(Account account) {
+ mAccount = account;
+ }
+
+ public Account getAccount() {
+ return mAccount;
+ }
+
+ public String getCallTypeString() {
+ switch (mCallType) {
+ case direction.CALL_TYPE_INCOMING:
+ return "CALL_TYPE_INCOMING";
+ case direction.CALL_TYPE_OUTGOING:
+ return "CALL_TYPE_OUTGOING";
+ default:
+ return "CALL_TYPE_UNDETERMINED";
+ }
+ }
+
+ public void setCallState(int callState) {
+ mCallState = callState;
+ }
+
+ public CallContact getmContact() {
+ return mContact;
+ }
+
+ public String getCallStateString() {
+
+ String text_state;
+
+ switch (mCallState) {
+ case state.CALL_STATE_NONE:
+ text_state = "NONE";
+ break;
+ case state.CALL_STATE_RINGING:
+ text_state = "RINGING";
+ break;
+ case state.CALL_STATE_CURRENT:
+ text_state = "CURRENT";
+ break;
+ case state.CALL_STATE_HUNGUP:
+ text_state = "HUNGUP";
+ break;
+ case state.CALL_STATE_BUSY:
+ text_state = "BUSY";
+ break;
+ case state.CALL_STATE_FAILURE:
+ text_state = "FAILURE";
+ break;
+ case state.CALL_STATE_HOLD:
+ text_state = "HOLD";
+ break;
+ case state.CALL_STATE_UNHOLD:
+ text_state = "UNHOLD";
+ break;
+ default:
+ text_state = "NULL";
+ }
+
+ return text_state;
+ }
+
+ public boolean isRecording() {
+ return isRecording;
+ }
+
+ public void setRecording(boolean isRecording) {
+ this.isRecording = isRecording;
+ }
+
+ public void printCallInfo() {
+ Log.i(TAG, "CallInfo: CallID: " + mCallID);
+ Log.i(TAG, " AccountID: " + mAccount.getAccountID());
+ Log.i(TAG, " CallState: " + mCallState);
+ Log.i(TAG, " CallType: " + mCallType);
+ }
+
+ /**
+ * Compare sip calls based on call ID
+ */
+ @Override
+ public boolean equals(Object c) {
+ return c instanceof SipCall && ((SipCall) c).mCallID.contentEquals((mCallID));
+ }
+
+ public boolean isOutGoing() {
+ return mCallType == direction.CALL_TYPE_OUTGOING;
+ }
+
+ public boolean isRinging() {
+ return mCallState == state.CALL_STATE_RINGING || mCallState == state.CALL_STATE_NONE;
+ }
+
+ public boolean isIncoming() {
+ return mCallType == direction.CALL_TYPE_INCOMING;
+ }
+
+ public boolean isOngoing() {
+ return !(mCallState == state.CALL_STATE_RINGING || mCallState == state.CALL_STATE_NONE || mCallState == state.CALL_STATE_FAILURE
+ || mCallState == state.CALL_STATE_BUSY || mCallState == state.CALL_STATE_HUNGUP);
+
+ }
+
+ public boolean isOnHold() {
+ return mCallState == state.CALL_STATE_HOLD;
+ }
+
+ public boolean isCurrent() {
+ return mCallState == state.CALL_STATE_CURRENT;
+ }
+
+
+}
diff --git a/ring-android/src/cx/ring/model/SipMessage.aidl b/ring-android/src/cx/ring/model/SipMessage.aidl
new file mode 100644
index 0000000..0729146
--- /dev/null
+++ b/ring-android/src/cx/ring/model/SipMessage.aidl
@@ -0,0 +1,4 @@
+package cx.ring.model;
+
+
+parcelable SipMessage;
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/model/SipMessage.java b/ring-android/src/cx/ring/model/SipMessage.java
new file mode 100644
index 0000000..f8a43cc
--- /dev/null
+++ b/ring-android/src/cx/ring/model/SipMessage.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.model;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class SipMessage implements Parcelable {
+ public boolean left;
+ public String comment;
+
+ public SipMessage(boolean left, String comment) {
+ super();
+ this.left = left;
+ this.comment = comment;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeByte((byte) (left ? 1 : 0));
+ out.writeString(comment);
+ }
+
+ public static final Parcelable.Creator<SipMessage> CREATOR = new Parcelable.Creator<SipMessage>() {
+ public SipMessage createFromParcel(Parcel in) {
+ return new SipMessage(in);
+ }
+
+ public SipMessage[] newArray(int size) {
+ return new SipMessage[size];
+ }
+ };
+
+ private SipMessage(Parcel in) {
+ left = (in.readByte() == 1);
+ comment = in.readString();
+ }
+
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/model/account/Account.java b/ring-android/src/cx/ring/model/account/Account.java
new file mode 100644
index 0000000..c129dde
--- /dev/null
+++ b/ring-android/src/cx/ring/model/account/Account.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model.account;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class Account extends java.util.Observable implements Parcelable {
+
+ String accountID;
+ private AccountDetailBasic basicDetails = null;
+ private AccountDetailAdvanced advancedDetails = null;
+ private AccountDetailSrtp srtpDetails = null;
+ private AccountDetailTls tlsDetails = null;
+ private ArrayList<AccountCredentials> credentialsDetails;
+
+ public Account(String bAccountID, HashMap<String, String> details, ArrayList<HashMap<String, String>> credentials) {
+ accountID = bAccountID;
+ basicDetails = new AccountDetailBasic(details);
+ advancedDetails = new AccountDetailAdvanced(details);
+ srtpDetails = new AccountDetailSrtp(details);
+ tlsDetails = new AccountDetailTls(details);
+ credentialsDetails = new ArrayList<AccountCredentials>();
+ for (int i = 0; i < credentials.size(); ++i) {
+ credentialsDetails.add(new AccountCredentials(credentials.get(i)));
+ }
+ }
+
+ public String getAccountID() {
+ return accountID;
+ }
+
+ public void setAccountID(String accountID) {
+ this.accountID = accountID;
+ }
+
+ public String getHost() {
+ return basicDetails.getDetailString(AccountDetailBasic.CONFIG_ACCOUNT_HOSTNAME);
+ }
+
+ public void setHost(String host) {
+ basicDetails.setDetailString(AccountDetailBasic.CONFIG_ACCOUNT_HOSTNAME, host);
+ }
+
+ public String getRegistered_state() {
+ return advancedDetails.getDetailString(AccountDetailAdvanced.CONFIG_ACCOUNT_REGISTRATION_STATUS);
+ }
+
+ public void setRegistered_state(String registered_state) {
+ advancedDetails.setDetailString(AccountDetailAdvanced.CONFIG_ACCOUNT_REGISTRATION_STATUS, registered_state);
+ }
+
+ public String getAlias() {
+ return basicDetails.getDetailString(AccountDetailBasic.CONFIG_ACCOUNT_ALIAS);
+ }
+
+ public Boolean isSip() {
+ return basicDetails.getDetailString(AccountDetailBasic.CONFIG_ACCOUNT_TYPE).equals("SIP");
+ }
+
+ public void setAlias(String alias) {
+ basicDetails.setDetailString(AccountDetailBasic.CONFIG_ACCOUNT_ALIAS, alias);
+ }
+
+ public Account(Parcel in) {
+ readFromParcel(in);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int arg1) {
+
+ dest.writeString(accountID);
+ dest.writeSerializable(basicDetails.getDetailsHashMap());
+ dest.writeSerializable(advancedDetails.getDetailsHashMap());
+ dest.writeSerializable(srtpDetails.getDetailsHashMap());
+ dest.writeSerializable(tlsDetails.getDetailsHashMap());
+ dest.writeInt(credentialsDetails.size());
+ for (AccountCredentials cred : credentialsDetails) {
+ dest.writeSerializable(cred.getDetailsHashMap());
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void readFromParcel(Parcel in) {
+
+ accountID = in.readString();
+ basicDetails = new AccountDetailBasic((HashMap<String, String>) in.readSerializable());
+ advancedDetails = new AccountDetailAdvanced((HashMap<String, String>) in.readSerializable());
+ srtpDetails = new AccountDetailSrtp((HashMap<String, String>) in.readSerializable());
+ tlsDetails = new AccountDetailTls((HashMap<String, String>) in.readSerializable());
+ credentialsDetails = new ArrayList<AccountCredentials>();
+ int cred_count = in.readInt();
+ for (int i = 0; i < cred_count; ++i) {
+ credentialsDetails.add(new AccountCredentials((HashMap<String, String>) in.readSerializable()));
+ }
+ }
+
+ public static final Parcelable.Creator<Account> CREATOR = new Parcelable.Creator<Account>() {
+ @Override
+ public Account createFromParcel(Parcel in) {
+ return new Account(in);
+ }
+
+ @Override
+ public Account[] newArray(int size) {
+ return new Account[size];
+ }
+ };
+
+ public AccountDetailBasic getBasicDetails() {
+ return basicDetails;
+ }
+
+ public void setBasicDetails(AccountDetailBasic basicDetails) {
+ this.basicDetails = basicDetails;
+ }
+
+ public AccountDetailAdvanced getAdvancedDetails() {
+ return advancedDetails;
+ }
+
+ public void setAdvancedDetails(AccountDetailAdvanced advancedDetails) {
+ this.advancedDetails = advancedDetails;
+ }
+
+ public AccountDetailSrtp getSrtpDetails() {
+ return srtpDetails;
+ }
+
+ public void setSrtpDetails(AccountDetailSrtp srtpDetails) {
+ this.srtpDetails = srtpDetails;
+ }
+
+ public AccountDetailTls getTlsDetails() {
+ return tlsDetails;
+ }
+
+ public void setTlsDetails(AccountDetailTls tlsDetails) {
+ this.tlsDetails = tlsDetails;
+ }
+
+ public boolean isEnabled() {
+ return (basicDetails.getDetailString(AccountDetailBasic.CONFIG_ACCOUNT_ENABLE).contentEquals(AccountDetail.TRUE_STR));
+ }
+
+ public void setEnabled(boolean isChecked) {
+ basicDetails.setDetailString(AccountDetailBasic.CONFIG_ACCOUNT_ENABLE, (isChecked ? AccountDetail.TRUE_STR
+ : AccountDetail.FALSE_STR));
+ }
+
+ public HashMap<String, String> getDetails() {
+ HashMap<String, String> results = new HashMap<String, String>();
+
+ results.putAll(basicDetails.getDetailsHashMap());
+ results.putAll(advancedDetails.getDetailsHashMap());
+ results.putAll(tlsDetails.getDetailsHashMap());
+ results.putAll(srtpDetails.getDetailsHashMap());
+ return results;
+ }
+
+ public boolean isRegistered() {
+ // FIXME Hardcoded values
+ return (getRegistered_state().contentEquals("REGISTERED") || getRegistered_state().contentEquals("OK"));
+ }
+
+ public boolean isIP2IP() {
+ return basicDetails.getDetailString(AccountDetailBasic.CONFIG_ACCOUNT_ALIAS).contentEquals("IP2IP");
+ }
+
+ public boolean isAutoanswerEnabled() {
+ return basicDetails.getDetailString(AccountDetailBasic.CONFIG_ACCOUNT_AUTOANSWER).contentEquals("true");
+ }
+
+ public ArrayList<AccountCredentials> getCredentials() {
+ return credentialsDetails;
+ }
+
+ public void addCredential(AccountCredentials newValue) {
+ credentialsDetails.add(newValue);
+ }
+
+ public void removeCredential(AccountCredentials accountCredentials) {
+ credentialsDetails.remove(accountCredentials);
+ }
+
+ @Override
+ public boolean hasChanged() {
+ return true;
+ }
+
+ public List getCredentialsHashMapList() {
+ ArrayList<HashMap<String, String>> result = new ArrayList<HashMap<String, String>>();
+ for (AccountCredentials cred : credentialsDetails) {
+ result.add(cred.getDetailsHashMap());
+ }
+ return result;
+ }
+
+ public boolean hasSDESEnabled() {
+ return srtpDetails.getDetailString(AccountDetailSrtp.CONFIG_SRTP_KEY_EXCHANGE).contentEquals("sdes");
+ }
+
+ public boolean useSecureLayer() {
+ return getSrtpDetails().getDetailBoolean(AccountDetailSrtp.CONFIG_SRTP_ENABLE) || getTlsDetails().getDetailBoolean(AccountDetailTls.CONFIG_TLS_ENABLE);
+ }
+}
diff --git a/ring-android/src/cx/ring/model/account/AccountCredentials.java b/ring-android/src/cx/ring/model/account/AccountCredentials.java
new file mode 100644
index 0000000..bd0eebb
--- /dev/null
+++ b/ring-android/src/cx/ring/model/account/AccountCredentials.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * If you own a pjsip commercial license you can also redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public License
+ * as an android library.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.model.account;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class AccountCredentials implements AccountDetail {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = AccountCredentials.class.getSimpleName();
+
+ public static final String CONFIG_ACCOUNT_USERNAME = "Account.username";
+ public static final String CONFIG_ACCOUNT_PASSWORD = "Account.password";
+ public static final String CONFIG_ACCOUNT_REALM = "Account.realm";
+
+ private ArrayList<AccountDetail.PreferenceEntry> privateArray;
+
+ public static ArrayList<AccountDetail.PreferenceEntry> getPreferenceEntries() {
+ ArrayList<AccountDetail.PreferenceEntry> preference = new ArrayList<AccountDetail.PreferenceEntry>();
+
+ preference.add(new PreferenceEntry(CONFIG_ACCOUNT_USERNAME));
+ preference.add(new PreferenceEntry(CONFIG_ACCOUNT_PASSWORD));
+ preference.add(new PreferenceEntry(CONFIG_ACCOUNT_REALM));
+
+ return preference;
+ }
+
+ public AccountCredentials(HashMap<String, String> pref) {
+ privateArray = getPreferenceEntries();
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ p.mValue = pref.get(p.mKey);
+ }
+
+ }
+
+ public ArrayList<AccountDetail.PreferenceEntry> getDetailValues() {
+ return privateArray;
+ }
+
+ public ArrayList<String> getValuesOnly() {
+ ArrayList<String> valueList = new ArrayList<String>();
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ valueList.add(p.mValue);
+ }
+
+ return valueList;
+ }
+
+ public HashMap<String, String> getDetailsHashMap() {
+ HashMap<String, String> map = new HashMap<String, String>();
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ map.put(p.mKey, p.mValue);
+ }
+
+ return map;
+ }
+
+ public String getDetailString(String key) {
+ String value = "";
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ if (p.mKey.equals(key)) {
+ value = p.mValue;
+ return value;
+ }
+ }
+ return value;
+ }
+
+ public void setDetailString(String key, String newValue) {
+ for (int i = 0; i < privateArray.size(); ++i) {
+ PreferenceEntry p = privateArray.get(i);
+ if (p.mKey.equals(key)) {
+ privateArray.get(i).mValue = newValue;
+ }
+ }
+
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof AccountCredentials)
+ return ((AccountCredentials) other).getDetailsHashMap().get(CONFIG_ACCOUNT_USERNAME)
+ .contentEquals(getDetailString(CONFIG_ACCOUNT_USERNAME))
+ && ((AccountCredentials) other).getDetailsHashMap().get(CONFIG_ACCOUNT_PASSWORD)
+ .contentEquals(getDetailString(CONFIG_ACCOUNT_PASSWORD))
+ && ((AccountCredentials) other).getDetailsHashMap().get(CONFIG_ACCOUNT_REALM)
+ .contentEquals(getDetailString(CONFIG_ACCOUNT_REALM));
+
+ return false;
+ }
+
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/model/account/AccountDetail.java b/ring-android/src/cx/ring/model/account/AccountDetail.java
new file mode 100644
index 0000000..54fce72
--- /dev/null
+++ b/ring-android/src/cx/ring/model/account/AccountDetail.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * If you own a pjsip commercial license you can also redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public License
+ * as an android library.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.model.account;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public interface AccountDetail {
+
+ public static final String TRUE_STR = "true";
+ public static final String FALSE_STR = "false";
+
+ public static class PreferenceEntry {
+ public String mKey;
+ public boolean isTwoState;
+ public String mValue;
+
+ public PreferenceEntry(String key) {
+ mKey = key;
+ isTwoState = false;
+ mValue = "";
+ }
+
+ public PreferenceEntry(String key, boolean twoState) {
+ mKey = key;
+ isTwoState = twoState;
+ mValue = "";
+ }
+
+ public boolean isChecked() {
+ return mValue.contentEquals("true");
+ }
+ }
+
+ public static final String TAG = "PreferenceHashMap";
+
+ public ArrayList<PreferenceEntry> getDetailValues();
+
+ public ArrayList<String> getValuesOnly();
+
+ public HashMap<String, String> getDetailsHashMap();
+
+ public String getDetailString(String key);
+
+ public void setDetailString(String key, String newValue);
+
+}
diff --git a/ring-android/src/cx/ring/model/account/AccountDetailAdvanced.java b/ring-android/src/cx/ring/model/account/AccountDetailAdvanced.java
new file mode 100644
index 0000000..2d09ba5
--- /dev/null
+++ b/ring-android/src/cx/ring/model/account/AccountDetailAdvanced.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * If you own a pjsip commercial license you can also redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public License
+ * as an android library.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.model.account;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import android.util.Log;
+
+public class AccountDetailAdvanced implements AccountDetail {
+
+ private static final String TAG = "AccountDetailAdvanced";
+
+ public static final String CONFIG_ACCOUNT_MAILBOX = "Account.mailbox";
+ public static final String CONFIG_ACCOUNT_REGISTRATION_EXPIRE = "Account.registrationExpire";
+ public static final String CONFIG_ACCOUNT_REGISTRATION_STATUS = "Account.registrationStatus";
+ public static final String CONFIG_ACCOUNT_REGISTRATION_STATE_CODE = "Account.registrationCode";
+ public static final String CONFIG_ACCOUNT_REGISTRATION_STATE_DESC = "Account.registrationDescription";
+ public static final String CONFIG_CREDENTIAL_NUMBER = "Credential.count";
+ public static final String CONFIG_ACCOUNT_DTMF_TYPE = "Account.dtmfType";
+ public static final String CONFIG_RINGTONE_PATH = "Account.ringtonePath";
+ public static final String CONFIG_RINGTONE_ENABLED = "Account.ringtoneEnabled";
+ public static final String CONFIG_KEEP_ALIVE_ENABLED = "Account.keepAliveEnabled";
+
+ public static final String CONFIG_LOCAL_INTERFACE = "Account.localInterface";
+ public static final String CONFIG_PUBLISHED_SAMEAS_LOCAL = "Account.publishedSameAsLocal";
+ public static final String CONFIG_LOCAL_PORT = "Account.localPort";
+ public static final String CONFIG_PUBLISHED_PORT = "Account.publishedPort";
+ public static final String CONFIG_PUBLISHED_ADDRESS = "Account.publishedAddress";
+
+ public static final String CONFIG_STUN_SERVER = "STUN.server";
+ public static final String CONFIG_STUN_ENABLE = "STUN.enable";
+
+ public static final String CONFIG_AUDIO_PORT_MIN = "Account.audioPortMin";
+ public static final String CONFIG_AUDIO_PORT_MAX = "Account.audioPortMax";
+
+ private ArrayList<AccountDetail.PreferenceEntry> privateArray;
+
+ public AccountDetailAdvanced(HashMap<String, String> pref) {
+ privateArray = new ArrayList<AccountDetail.PreferenceEntry>();
+
+ for (String key : pref.keySet()) {
+ PreferenceEntry p = new PreferenceEntry(key);
+ p.mValue = pref.get(key);
+
+ if(key.contentEquals(CONFIG_RINGTONE_ENABLED) ||
+ key.contentEquals(CONFIG_KEEP_ALIVE_ENABLED) ||
+ key.contentEquals(CONFIG_PUBLISHED_SAMEAS_LOCAL) ||
+ key.contentEquals(CONFIG_STUN_ENABLE))
+ p.isTwoState = true;
+
+ privateArray.add(p);
+ }
+ }
+
+ public ArrayList<AccountDetail.PreferenceEntry> getDetailValues() {
+ return privateArray;
+ }
+
+ public ArrayList<String> getValuesOnly() {
+ ArrayList<String> valueList = new ArrayList<String>();
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ Log.i(TAG, "" + p.mValue);
+ valueList.add(p.mValue);
+ }
+
+ return valueList;
+ }
+
+ public HashMap<String, String> getDetailsHashMap() {
+ HashMap<String, String> map = new HashMap<String, String>();
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ map.put(p.mKey, p.mValue);
+ }
+
+ return map;
+ }
+
+ public String getDetailString(String key) {
+ String value = "";
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ if (p.mKey.equals(key)) {
+ value = p.mValue;
+ return value;
+ }
+ }
+ return value;
+ }
+
+ public void setDetailString(String key, String newValue) {
+ for (PreferenceEntry p : privateArray) {
+ if (p.mKey.equals(key)) {
+ p.mValue = newValue;
+ }
+ }
+
+ }
+
+}
diff --git a/ring-android/src/cx/ring/model/account/AccountDetailBasic.java b/ring-android/src/cx/ring/model/account/AccountDetailBasic.java
new file mode 100644
index 0000000..bec769e
--- /dev/null
+++ b/ring-android/src/cx/ring/model/account/AccountDetailBasic.java
@@ -0,0 +1,111 @@
+/**
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * If you own a pjsip commercial license you can also redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public License
+ * as an android library.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.model.account;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import android.util.Log;
+
+public class AccountDetailBasic implements AccountDetail {
+
+ private static final String TAG = AccountDetailBasic.class.getSimpleName();
+
+ public static final String CONFIG_ACCOUNT_ALIAS = "Account.alias";
+ public static final String CONFIG_ACCOUNT_HOSTNAME = "Account.hostname";
+ public static final String CONFIG_ACCOUNT_USERNAME = "Account.username";
+ public static final String CONFIG_ACCOUNT_PASSWORD = "Account.password";
+
+ public static final String CONFIG_ACCOUNT_USERAGENT = "Account.useragent";
+ public static final String CONFIG_ACCOUNT_ROUTESET = "Account.routeset";
+ public static final String CONFIG_ACCOUNT_AUTOANSWER = "Account.autoAnswer";
+
+ public static final String CONFIG_ACCOUNT_REALM = "Account.realm";
+ public static final String CONFIG_ACCOUNT_TYPE = "Account.type";
+ public static final String CONFIG_ACCOUNT_ENABLE = "Account.enable";
+ public static final String CONFIG_PRESENCE_ENABLE = "Account.presenceEnabled";
+
+ private ArrayList<AccountDetail.PreferenceEntry> privateArray;
+
+ public AccountDetailBasic(HashMap<String, String> pref) {
+ privateArray = new ArrayList<AccountDetail.PreferenceEntry>();
+
+ for (String key : pref.keySet()) {
+ PreferenceEntry p = new PreferenceEntry(key);
+ p.mValue = pref.get(key);
+
+ if(key.contentEquals(CONFIG_ACCOUNT_ENABLE) || key.contentEquals(CONFIG_ACCOUNT_AUTOANSWER))
+ p.isTwoState = true;
+
+ privateArray.add(p);
+ }
+ }
+
+ public ArrayList<AccountDetail.PreferenceEntry> getDetailValues() {
+ return privateArray;
+ }
+
+ public ArrayList<String> getValuesOnly() {
+ ArrayList<String> valueList = new ArrayList<String>();
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ Log.i(TAG, "" + p.mValue);
+ valueList.add(p.mValue);
+ }
+
+ return valueList;
+ }
+
+ public HashMap<String, String> getDetailsHashMap() {
+ HashMap<String, String> map = new HashMap<String, String>();
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ map.put(p.mKey, p.mValue);
+ }
+
+ return map;
+ }
+
+ public String getDetailString(String key) {
+ String value = "";
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ if (p.mKey.equals(key)) {
+ value = p.mValue;
+ return value;
+ }
+ }
+
+ return value;
+ }
+
+ public void setDetailString(String key, String newValue) {
+ for (int i = 0; i < privateArray.size(); ++i) {
+ PreferenceEntry p = privateArray.get(i);
+ if (p.mKey.equals(key)) {
+ privateArray.get(i).mValue = newValue;
+ }
+ }
+
+ }
+
+}
diff --git a/ring-android/src/cx/ring/model/account/AccountDetailSrtp.java b/ring-android/src/cx/ring/model/account/AccountDetailSrtp.java
new file mode 100644
index 0000000..29b3ef5
--- /dev/null
+++ b/ring-android/src/cx/ring/model/account/AccountDetailSrtp.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * If you own a pjsip commercial license you can also redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public License
+ * as an android library.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.model.account;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class AccountDetailSrtp implements AccountDetail {
+
+ private static final String TAG = "AccountDetailSrtp";
+
+ public static final String CONFIG_SRTP_ENABLE = "SRTP.enable";
+ public static final String CONFIG_SRTP_KEY_EXCHANGE = "SRTP.keyExchange";
+ public static final String CONFIG_SRTP_ENCRYPTION_ALGO = "SRTP.encryptionAlgorithm"; // Provided by ccRTP,0=NULL,1=AESCM,2=AESF8
+ public static final String CONFIG_SRTP_RTP_FALLBACK = "SRTP.rtpFallback";
+ public static final String CONFIG_ZRTP_HELLO_HASH = "ZRTP.helloHashEnable";
+ public static final String CONFIG_ZRTP_DISPLAY_SAS = "ZRTP.displaySAS";
+ public static final String CONFIG_ZRTP_NOT_SUPP_WARNING = "ZRTP.notSuppWarning";
+ public static final String CONFIG_ZRTP_DISPLAY_SAS_ONCE = "ZRTP.displaySasOnce";
+
+ private ArrayList<AccountDetail.PreferenceEntry> privateArray;
+
+ public static ArrayList<AccountDetail.PreferenceEntry> getPreferenceEntries() {
+ ArrayList<AccountDetail.PreferenceEntry> preference = new ArrayList<AccountDetail.PreferenceEntry>();
+
+ preference.add(new PreferenceEntry(CONFIG_SRTP_ENABLE, true));
+ preference.add(new PreferenceEntry(CONFIG_SRTP_KEY_EXCHANGE, false));
+ preference.add(new PreferenceEntry(CONFIG_SRTP_ENCRYPTION_ALGO, true));
+ preference.add(new PreferenceEntry(CONFIG_SRTP_RTP_FALLBACK, true));
+ preference.add(new PreferenceEntry(CONFIG_ZRTP_HELLO_HASH, true));
+ preference.add(new PreferenceEntry(CONFIG_ZRTP_DISPLAY_SAS, true));
+ preference.add(new PreferenceEntry(CONFIG_ZRTP_NOT_SUPP_WARNING, true));
+ preference.add(new PreferenceEntry(CONFIG_ZRTP_DISPLAY_SAS_ONCE, true));
+
+ return preference;
+ }
+
+ public AccountDetailSrtp(HashMap<String, String> pref) {
+ privateArray = getPreferenceEntries();
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ p.mValue = pref.get(p.mKey);
+ }
+ }
+
+ public ArrayList<AccountDetail.PreferenceEntry> getDetailValues() {
+ return privateArray;
+ }
+
+ public ArrayList<String> getValuesOnly() {
+ ArrayList<String> valueList = new ArrayList<String>();
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ valueList.add(p.mValue);
+ }
+
+ return valueList;
+ }
+
+ public HashMap<String, String> getDetailsHashMap() {
+ HashMap<String, String> map = new HashMap<String, String>();
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ if (p.mValue == null) {
+ map.put(p.mKey, "");
+ } else {
+ map.put(p.mKey, p.mValue);
+ }
+ }
+
+ return map;
+ }
+
+ public String getDetailString(String key) {
+ String value = "";
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ if (p.mKey.equals(key)) {
+ value = p.mValue;
+ return value;
+ }
+ }
+
+ return value;
+ }
+
+ public void setDetailString(String key, String newValue) {
+ for (int i = 0; i < privateArray.size(); ++i) {
+ PreferenceEntry p = privateArray.get(i);
+ if (p.mKey.equals(key)) {
+ privateArray.get(i).mValue = newValue;
+ }
+ }
+
+ }
+
+ public boolean getDetailBoolean(String srtpParam) {
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ if (p.mKey.equals(srtpParam)) {
+ return p.mValue.contentEquals("true");
+ }
+ }
+ return false;
+ }
+}
diff --git a/ring-android/src/cx/ring/model/account/AccountDetailTls.java b/ring-android/src/cx/ring/model/account/AccountDetailTls.java
new file mode 100644
index 0000000..37054b6
--- /dev/null
+++ b/ring-android/src/cx/ring/model/account/AccountDetailTls.java
@@ -0,0 +1,134 @@
+/**
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * If you own a pjsip commercial license you can also redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public License
+ * as an android library.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.model.account;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class AccountDetailTls implements AccountDetail {
+
+ private static final String TAG = "AccountDetailTls";
+
+ public static final String CONFIG_TLS_LISTENER_PORT = "TLS.listenerPort";
+ public static final String CONFIG_TLS_ENABLE = "TLS.enable";
+ public static final String CONFIG_TLS_CA_LIST_FILE = "TLS.certificateListFile";
+ public static final String CONFIG_TLS_CERTIFICATE_FILE = "TLS.certificateFile";
+ public static final String CONFIG_TLS_PRIVATE_KEY_FILE = "TLS.privateKeyFile";
+ public static final String CONFIG_TLS_PASSWORD = "TLS.password";
+ public static final String CONFIG_TLS_METHOD = "TLS.method";
+ public static final String CONFIG_TLS_CIPHERS = "TLS.ciphers";
+ public static final String CONFIG_TLS_SERVER_NAME = "TLS.serverName";
+ public static final String CONFIG_TLS_VERIFY_SERVER = "TLS.verifyServer";
+ public static final String CONFIG_TLS_VERIFY_CLIENT = "TLS.verifyClient";
+ public static final String CONFIG_TLS_REQUIRE_CLIENT_CERTIFICATE = "TLS.requireClientCertificate";
+ public static final String CONFIG_TLS_NEGOTIATION_TIMEOUT_SEC = "TLS.negotiationTimeoutSec";
+
+ private ArrayList<AccountDetail.PreferenceEntry> privateArray;
+
+ public static ArrayList<AccountDetail.PreferenceEntry> getPreferenceEntries() {
+ ArrayList<AccountDetail.PreferenceEntry> preference = new ArrayList<AccountDetail.PreferenceEntry>();
+
+ preference.add(new PreferenceEntry(CONFIG_TLS_LISTENER_PORT));
+ preference.add(new PreferenceEntry(CONFIG_TLS_ENABLE, true));
+ preference.add(new PreferenceEntry(CONFIG_TLS_CA_LIST_FILE));
+ preference.add(new PreferenceEntry(CONFIG_TLS_CERTIFICATE_FILE));
+ preference.add(new PreferenceEntry(CONFIG_TLS_PRIVATE_KEY_FILE));
+ preference.add(new PreferenceEntry(CONFIG_TLS_PASSWORD));
+ preference.add(new PreferenceEntry(CONFIG_TLS_METHOD));
+ preference.add(new PreferenceEntry(CONFIG_TLS_CIPHERS));
+ preference.add(new PreferenceEntry(CONFIG_TLS_SERVER_NAME));
+ preference.add(new PreferenceEntry(CONFIG_TLS_VERIFY_SERVER));
+ preference.add(new PreferenceEntry(CONFIG_TLS_VERIFY_CLIENT, true));
+ preference.add(new PreferenceEntry(CONFIG_TLS_REQUIRE_CLIENT_CERTIFICATE, true));
+ preference.add(new PreferenceEntry(CONFIG_TLS_NEGOTIATION_TIMEOUT_SEC));
+
+ return preference;
+ }
+
+ public AccountDetailTls(HashMap<String, String> pref) {
+ privateArray = getPreferenceEntries();
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ p.mValue = pref.get(p.mKey);
+ }
+ }
+
+ public ArrayList<AccountDetail.PreferenceEntry> getDetailValues() {
+ return privateArray;
+ }
+
+ public ArrayList<String> getValuesOnly() {
+ ArrayList<String> valueList = new ArrayList<String>();
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ valueList.add(p.mValue);
+ }
+
+ return valueList;
+ }
+
+ public HashMap<String, String> getDetailsHashMap() {
+ HashMap<String, String> map = new HashMap<String, String>();
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ if (p.mValue == null) {
+ map.put(p.mKey, "");
+ } else {
+ map.put(p.mKey, p.mValue);
+ }
+ }
+
+ return map;
+ }
+
+ public String getDetailString(String key) {
+ String value = "";
+
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ if (p.mKey.equals(key)) {
+ value = p.mValue;
+ return value;
+ }
+ }
+
+ return value;
+ }
+
+ public void setDetailString(String key, String newValue) {
+ for (int i = 0; i < privateArray.size(); ++i) {
+ PreferenceEntry p = privateArray.get(i);
+ if (p.mKey.equals(key)) {
+ privateArray.get(i).mValue = newValue;
+ }
+ }
+
+ }
+
+ public boolean getDetailBoolean(String key) {
+ for (AccountDetail.PreferenceEntry p : privateArray) {
+ if (p.mKey.equals(key)) {
+ return p.mValue.contentEquals("true");
+ }
+ }
+ return false;
+ }
+}
diff --git a/ring-android/src/cx/ring/model/account/CredentialsManager.java b/ring-android/src/cx/ring/model/account/CredentialsManager.java
new file mode 100644
index 0000000..f3ae1a2
--- /dev/null
+++ b/ring-android/src/cx/ring/model/account/CredentialsManager.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model.account;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import cx.ring.views.CredentialsPreference;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.PreferenceScreen;
+
+public class CredentialsManager {
+
+ PreferenceScreen mScreen;
+ public static final String CURRENT_CRED = "current_cred";
+ public static final String NEW_CRED = "new_cred";
+ private Context mContext;
+ private Account mAccount;
+
+
+
+ public void onCreate(Context cont, PreferenceScreen preferenceScreen, Account acc) {
+ mContext = cont;
+ mScreen = preferenceScreen;
+ mAccount = acc;
+ }
+
+ public void reloadCredentials() {
+ removeAllCredentials();
+ addAllCredentials();
+ }
+
+ public void setAddCredentialListener() {
+ mScreen.findPreference("Add.credentials").setOnPreferenceChangeListener(addCredentialListener);
+ }
+
+ public void setEditCredentialListener() {
+ mScreen.findPreference("Add.credentials").setOnPreferenceChangeListener(addCredentialListener);
+ }
+
+ private void addAllCredentials() {
+ ArrayList<AccountCredentials> credentials = mAccount.getCredentials();
+ for (AccountCredentials cred : credentials) {
+ CredentialsPreference toAdd = new CredentialsPreference(mContext, null);
+ toAdd.setKey("credential");
+ toAdd.setTitle(cred.getDetailString(AccountCredentials.CONFIG_ACCOUNT_USERNAME));
+ toAdd.setSummary(cred.getDetailString(AccountCredentials.CONFIG_ACCOUNT_REALM));
+ toAdd.getExtras().putSerializable(CURRENT_CRED, cred.getDetailsHashMap());
+ toAdd.setOnPreferenceChangeListener(editCredentialListener);
+ toAdd.setIcon(null);
+ mScreen.addPreference(toAdd);
+ }
+
+ }
+
+ private void removeAllCredentials() {
+ Preference toRemove = mScreen.findPreference("credential");
+ while (mScreen.findPreference("credential") != null) {
+ mScreen.removePreference(toRemove);
+ toRemove = mScreen.findPreference("credential");
+ }
+ }
+
+ private OnPreferenceChangeListener editCredentialListener = new OnPreferenceChangeListener() {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+
+ // We need the old and new value to correctly edit the list of credentials
+ Bundle result = (Bundle) newValue;
+ mAccount.removeCredential(new AccountCredentials((HashMap<String, String>) result.get(CURRENT_CRED)));
+
+ if(result.get(NEW_CRED) != null){
+ // There is a new value for this credentials it means it has been edited (otherwise deleted)
+ mAccount.addCredential(new AccountCredentials((HashMap<String, String>) result.get(NEW_CRED)));
+ }
+ mAccount.notifyObservers();
+ reloadCredentials();
+ return false;
+ }
+ };
+
+ private OnPreferenceChangeListener addCredentialListener = new OnPreferenceChangeListener() {
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mAccount.addCredential((AccountCredentials) newValue);
+ mAccount.notifyObservers();
+ reloadCredentials();
+ return false;
+ }
+ };
+
+
+
+}
diff --git a/ring-android/src/cx/ring/model/account/SRTPManager.java b/ring-android/src/cx/ring/model/account/SRTPManager.java
new file mode 100644
index 0000000..1414d0d
--- /dev/null
+++ b/ring-android/src/cx/ring/model/account/SRTPManager.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model.account;
+
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.PreferenceScreen;
+import android.util.Log;
+
+public class SRTPManager {
+ PreferenceScreen mScreen;
+ private Account mAccount;
+
+ public void onCreate(PreferenceScreen preferenceScreen, Account acc) {
+ mScreen = preferenceScreen;
+ mAccount = acc;
+
+ setDetails();
+ }
+
+ private void setDetails() {
+ for (int i = 0; i < mScreen.getPreferenceCount(); ++i) {
+ ((CheckBoxPreference) mScreen.getPreference(i)).setChecked(mAccount.getSrtpDetails().getDetailBoolean(mScreen.getPreference(i).getKey()));
+ }
+ }
+
+ public void setSDESListener() {
+ mScreen.findPreference("SRTP.rtpFallback").setOnPreferenceChangeListener(toggleFallbackListener);
+ }
+
+ private OnPreferenceChangeListener toggleFallbackListener = new OnPreferenceChangeListener() {
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mAccount.getSrtpDetails().setDetailString(AccountDetailSrtp.CONFIG_SRTP_RTP_FALLBACK, Boolean.toString((Boolean) newValue));
+ mAccount.notifyObservers();
+ return true;
+ }
+ };
+
+ public void setZRTPListener() {
+ for (int i = 0; i < mScreen.getPreferenceCount(); ++i) {
+ mScreen.getPreference(i).setOnPreferenceChangeListener(zrtpListener);
+ }
+ }
+
+ private OnPreferenceChangeListener zrtpListener = new OnPreferenceChangeListener() {
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ Log.i("SRTP", "Setting " + preference.getKey() + " to" + (Boolean) newValue);
+ mAccount.getSrtpDetails().setDetailString(preference.getKey(), Boolean.toString((Boolean) newValue));
+ mAccount.notifyObservers();
+ return true;
+ }
+ };
+
+}
diff --git a/ring-android/src/cx/ring/model/account/TLSManager.java b/ring-android/src/cx/ring/model/account/TLSManager.java
new file mode 100644
index 0000000..c2f6dca
--- /dev/null
+++ b/ring-android/src/cx/ring/model/account/TLSManager.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.model.account;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceScreen;
+import android.util.Log;
+import cx.ring.R;
+import cx.ring.fragments.NestedSettingsFragment;
+
+import java.io.File;
+
+public class TLSManager {
+ private static final String TAG = TLSManager.class.getSimpleName();
+ private static final int SELECT_CA_LIST_RC = 42;
+ private static final int SELECT_PRIVATE_KEY_RC = 43;
+ private static final int SELECT_CERTIFICATE_RC = 44;
+ private OnPreferenceClickListener filePickerListener = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (preference.getKey().contentEquals(AccountDetailTls.CONFIG_TLS_CA_LIST_FILE)) {
+ performFileSearch(SELECT_CA_LIST_RC);
+ }
+ if (preference.getKey().contentEquals(AccountDetailTls.CONFIG_TLS_PRIVATE_KEY_FILE)) {
+ performFileSearch(SELECT_PRIVATE_KEY_RC);
+ }
+ if (preference.getKey().contentEquals(AccountDetailTls.CONFIG_TLS_CERTIFICATE_FILE)) {
+ performFileSearch(SELECT_CERTIFICATE_RC);
+ }
+ return true;
+ }
+ };
+ PreferenceScreen mScreen;
+ private Account mAccount;
+ private NestedSettingsFragment mFrag;
+ private OnPreferenceChangeListener tlsListener = new OnPreferenceChangeListener() {
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ Log.i("TLS", "Setting " + preference.getKey() + " to" + newValue);
+
+ if (preference.getKey().contentEquals(AccountDetailTls.CONFIG_TLS_ENABLE)) {
+ togglePreferenceScreen((Boolean) newValue);
+ if(((Boolean)newValue)){
+ mAccount.getAdvancedDetails().setDetailString(AccountDetailAdvanced.CONFIG_STUN_ENABLE, Boolean.toString(false));
+ }
+ }
+
+ if (preference instanceof CheckBoxPreference) {
+ mAccount.getTlsDetails().setDetailString(preference.getKey(), Boolean.toString((Boolean) newValue));
+ } else {
+ preference.setSummary((String) newValue);
+ mAccount.getTlsDetails().setDetailString(preference.getKey(), (String) newValue);
+ }
+ mAccount.notifyObservers();
+ return true;
+ }
+ };
+
+ public void onCreate(NestedSettingsFragment con, PreferenceScreen preferenceScreen, Account acc) {
+ mFrag = con;
+ mScreen = preferenceScreen;
+ mAccount = acc;
+ setDetails();
+ }
+
+ private void setDetails() {
+ boolean activated = mAccount.getTlsDetails().getDetailBoolean(AccountDetailTls.CONFIG_TLS_ENABLE);
+
+ for (int i = 0; i < mScreen.getPreferenceCount(); ++i) {
+
+ Preference current = mScreen.getPreference(i);
+
+ if (current instanceof CheckBoxPreference) {
+ ((CheckBoxPreference) mScreen.getPreference(i)).setChecked(mAccount.getTlsDetails().getDetailBoolean(
+ mScreen.getPreference(i).getKey()));
+ } else {
+ if (current.getKey().contentEquals(AccountDetailTls.CONFIG_TLS_CA_LIST_FILE)) {
+ File crt = new File(mAccount.getTlsDetails().getDetailString(AccountDetailTls.CONFIG_TLS_CA_LIST_FILE));
+ current.setSummary(crt.getName());
+ current.setOnPreferenceClickListener(filePickerListener);
+ setFeedbackIcon(current, crt.getAbsolutePath());
+ } else if (current.getKey().contentEquals(AccountDetailTls.CONFIG_TLS_PRIVATE_KEY_FILE)) {
+ current.setSummary(new File(mAccount.getTlsDetails().getDetailString(AccountDetailTls.CONFIG_TLS_PRIVATE_KEY_FILE)).getName());
+ current.setOnPreferenceClickListener(filePickerListener);
+ } else if (current.getKey().contentEquals(AccountDetailTls.CONFIG_TLS_CERTIFICATE_FILE)) {
+ File pem = new File(mAccount.getTlsDetails().getDetailString(AccountDetailTls.CONFIG_TLS_CERTIFICATE_FILE));
+ current.setSummary(pem.getName());
+ current.setOnPreferenceClickListener(filePickerListener);
+ setFeedbackIcon(current, pem.getAbsolutePath());
+ checkForRSAKey(pem.getAbsolutePath());
+ } else if (current.getKey().contentEquals(AccountDetailTls.CONFIG_TLS_METHOD)) {
+ String[] values = mFrag.getTlsMethods();
+ ((ListPreference)current).setEntries(values);
+ ((ListPreference)current).setEntryValues(values);
+ current.setSummary(mAccount.getTlsDetails().getDetailString(mScreen.getPreference(i).getKey()));
+ } else {
+ current.setSummary(mAccount.getTlsDetails().getDetailString(mScreen.getPreference(i).getKey()));
+ }
+ }
+
+ // First Preference should remain enabled, it's the actual switch TLS.enable
+ if (i > 0)
+ current.setEnabled(activated);
+
+ }
+ }
+
+ private void checkForRSAKey(String path) {
+ if(mFrag.findRSAKey(path)){
+ mScreen.findPreference(AccountDetailTls.CONFIG_TLS_PRIVATE_KEY_FILE).setEnabled(false);
+ }else {
+ mScreen.findPreference(AccountDetailTls.CONFIG_TLS_PRIVATE_KEY_FILE).setEnabled(true);
+ }
+ }
+
+ private void setFeedbackIcon(Preference current, String crtPath) {
+ if(!mFrag.checkCertificate(crtPath)){
+ current.setIcon(R.drawable.ic_error);
+ } else {
+ current.setIcon(R.drawable.ic_good);
+ }
+ }
+
+ public void setTLSListener() {
+ for (int i = 0; i < mScreen.getPreferenceCount(); ++i) {
+ mScreen.getPreference(i).setOnPreferenceChangeListener(tlsListener);
+ }
+ }
+
+ private void togglePreferenceScreen(Boolean state) {
+ for (int i = 1; i < mScreen.getPreferenceCount(); ++i) {
+ mScreen.getPreference(i).setEnabled(state);
+ }
+ }
+
+ public void performFileSearch(int requestCodeToSet) {
+
+ // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
+ // browser.
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+
+ // Filter to only show results that can be "opened", such as a
+ // file (as opposed to a list of contacts or timezones)
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+
+ // Filter to show only images, using the image MIME data type.
+ // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
+ // To search for all documents available via installed storage providers,
+ // it would be "*/*".
+ intent.setType("*/*");
+ mFrag.startActivityForResult(intent, requestCodeToSet);
+ }
+
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // TODO Extract returned filed for intent and populate correct preference
+
+ if (resultCode == Activity.RESULT_CANCELED)
+ return;
+
+ File myFile = new File(data.getData().getEncodedPath());
+ Preference pref;
+ switch (requestCode) {
+ case SELECT_CA_LIST_RC:
+ pref = mScreen.findPreference(AccountDetailTls.CONFIG_TLS_CA_LIST_FILE);
+ pref.setSummary(myFile.getName());
+ mAccount.getTlsDetails().setDetailString(AccountDetailTls.CONFIG_TLS_CA_LIST_FILE, myFile.getAbsolutePath());
+ mAccount.notifyObservers();
+ setFeedbackIcon(pref, myFile.getAbsolutePath());
+ break;
+ case SELECT_PRIVATE_KEY_RC:
+ mScreen.findPreference(AccountDetailTls.CONFIG_TLS_PRIVATE_KEY_FILE).setSummary(myFile.getName());
+ mAccount.getTlsDetails().setDetailString(AccountDetailTls.CONFIG_TLS_PRIVATE_KEY_FILE, myFile.getAbsolutePath());
+ mAccount.notifyObservers();
+ break;
+ case SELECT_CERTIFICATE_RC:
+ pref = mScreen.findPreference(AccountDetailTls.CONFIG_TLS_CERTIFICATE_FILE);
+ pref.setSummary(myFile.getName());
+ mAccount.getTlsDetails().setDetailString(AccountDetailTls.CONFIG_TLS_CERTIFICATE_FILE, myFile.getAbsolutePath());
+ mAccount.notifyObservers();
+ setFeedbackIcon(pref, myFile.getAbsolutePath());
+ checkForRSAKey(myFile.getAbsolutePath());
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/service/CallManagerCallBack.java b/ring-android/src/cx/ring/service/CallManagerCallBack.java
new file mode 100644
index 0000000..622f58f
--- /dev/null
+++ b/ring-android/src/cx/ring/service/CallManagerCallBack.java
@@ -0,0 +1,350 @@
+package cx.ring.service;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import cx.ring.client.CallActivity;
+import cx.ring.model.account.Account;
+import cx.ring.utils.SwigNativeConverter;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+import cx.ring.model.CallContact;
+import cx.ring.model.Conference;
+import cx.ring.model.SecureSipCall;
+import cx.ring.model.SipCall;
+import cx.ring.model.SipMessage;
+
+public class CallManagerCallBack extends Callback {
+
+ private static final String TAG = "CallManagerCallBack";
+ private SipService mService;
+
+ static public final String CALL_STATE_CHANGED = "call-state-changed";
+ static public final String INCOMING_CALL = "incoming-call";
+ static public final String INCOMING_TEXT = "incoming-text";
+ static public final String CONF_CREATED = "conf_created";
+ static public final String CONF_REMOVED = "conf_removed";
+ static public final String CONF_CHANGED = "conf_changed";
+ static public final String RECORD_STATE_CHANGED = "record_state";
+
+ static public final String ZRTP_ON = "secure_zrtp_on";
+ static public final String ZRTP_OFF = "secure_zrtp_off";
+ static public final String DISPLAY_SAS = "display_sas";
+ static public final String ZRTP_NEGOTIATION_FAILED = "zrtp_nego_failed";
+ static public final String ZRTP_NOT_SUPPORTED = "zrtp_not_supported";
+
+ static public final String RTCP_REPORT_RECEIVED = "on_rtcp_report_received";
+
+
+ public CallManagerCallBack(SipService context) {
+ super();
+ mService = context;
+ }
+
+ @Override
+ public void callOnStateChange(String callID, String newState) {
+ Log.d(TAG, "on_call_state_changed : (" + callID + ", " + newState + ")");
+
+ Conference toUpdate = mService.findConference(callID);
+
+ if (toUpdate == null) {
+ return;
+ }
+
+ Intent intent = new Intent(CALL_STATE_CHANGED);
+ intent.putExtra("CallID", callID);
+ intent.putExtra("State", newState);
+
+ if (newState.equals("RINGING")) {
+ toUpdate.setCallState(callID, SipCall.state.CALL_STATE_RINGING);
+ } else if (newState.equals("CURRENT")) {
+ if (toUpdate.isRinging()) {
+ toUpdate.getCallById(callID).setTimestampStart_(System.currentTimeMillis());
+ }
+ toUpdate.setCallState(callID, SipCall.state.CALL_STATE_CURRENT);
+ } else if (newState.equals("HUNGUP")) {
+ Log.d(TAG, "Hanging up " + callID);
+ SipCall call = toUpdate.getCallById(callID);
+ if (!toUpdate.hasMultipleParticipants()) {
+ if (toUpdate.isRinging() && toUpdate.isIncoming()) {
+ mService.mNotificationManager.publishMissedCallNotification(mService.getConferences().get(callID));
+ }
+ toUpdate.setCallState(callID, SipCall.state.CALL_STATE_HUNGUP);
+ mService.mHistoryManager.insertNewEntry(toUpdate);
+ mService.getConferences().remove(toUpdate.getId());
+ } else {
+ toUpdate.setCallState(callID, SipCall.state.CALL_STATE_HUNGUP);
+ mService.mHistoryManager.insertNewEntry(call);
+ }
+ } else if (newState.equals("BUSY")) {
+ toUpdate.setCallState(callID, SipCall.state.CALL_STATE_BUSY);
+ mService.getConferences().remove(toUpdate.getId());
+ } else if (newState.equals("FAILURE")) {
+ toUpdate.setCallState(callID, SipCall.state.CALL_STATE_FAILURE);
+ mService.getConferences().remove(toUpdate.getId());
+ Ringservice.sflph_call_hang_up(callID);
+ } else if (newState.equals("HOLD")) {
+ toUpdate.setCallState(callID, SipCall.state.CALL_STATE_HOLD);
+ } else if (newState.equals("UNHOLD")) {
+ toUpdate.setCallState(callID, SipCall.state.CALL_STATE_CURRENT);
+ }
+ intent.putExtra("conference", toUpdate);
+ mService.sendBroadcast(intent);
+ }
+
+
+ @Override
+ public void callOnIncomingCall(String accountID, String callID, String from) {
+ Log.d(TAG, "on_incoming_call(" + accountID + ", " + callID + ", " + from + ")");
+
+ try {
+ StringMap details = Ringservice.sflph_config_get_account_details(accountID);
+ VectMap credentials = Ringservice.sflph_config_get_credentials(accountID);
+ Account acc = new Account(accountID, SwigNativeConverter.convertAccountToNative(details), SwigNativeConverter.convertCredentialsToNative(credentials));
+
+ Bundle args = new Bundle();
+ args.putString(SipCall.ID, callID);
+ args.putParcelable(SipCall.ACCOUNT, acc);
+ args.putInt(SipCall.STATE, SipCall.state.CALL_STATE_RINGING);
+ args.putInt(SipCall.TYPE, SipCall.direction.CALL_TYPE_INCOMING);
+
+ CallContact unknow = CallContact.ContactBuilder.buildUnknownContact(from);
+ args.putParcelable(SipCall.CONTACT, unknow);
+
+ Intent toSend = new Intent(CallManagerCallBack.INCOMING_CALL);
+ toSend.setClass(mService, CallActivity.class);
+ toSend.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ SipCall newCall = new SipCall(args);
+ newCall.setTimestampStart_(System.currentTimeMillis());
+
+ Conference toAdd;
+ if (acc.useSecureLayer()) {
+ SecureSipCall secureCall = new SecureSipCall(newCall);
+ toAdd = new Conference(secureCall);
+ } else {
+ toAdd = new Conference(newCall);
+ }
+
+ mService.getConferences().put(toAdd.getId(), toAdd);
+
+ Bundle bundle = new Bundle();
+ bundle.putParcelable("conference", toAdd);
+ toSend.putExtra("resuming", false);
+ toSend.putExtras(bundle);
+ mService.startActivity(toSend);
+ mService.mMediaManager.startRing("");
+ mService.mMediaManager.obtainAudioFocus(true);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void callOnConferenceCreated(final String confID) {
+ Log.w(TAG, "CONFERENCE CREATED:" + confID);
+ Intent intent = new Intent(CONF_CREATED);
+ Conference created = new Conference(confID);
+
+ StringVect all_participants = Ringservice.sflph_call_get_participant_list(confID);
+ Log.w(TAG, "all_participants:" + all_participants.size());
+ for (int i = 0; i < all_participants.size(); ++i) {
+ if (mService.getConferences().get(all_participants.get(i)) != null) {
+ created.addParticipant(mService.getConferences().get(all_participants.get(i)).getCallById(all_participants.get(i)));
+ mService.getConferences().remove(all_participants.get(i));
+ } else {
+ for (Map.Entry<String, Conference> stringConferenceEntry : mService.getConferences().entrySet()) {
+ Conference tmp = stringConferenceEntry.getValue();
+ for (SipCall c : tmp.getParticipants()) {
+ if (c.getCallId().contentEquals(all_participants.get(i))) {
+ created.addParticipant(c);
+ mService.getConferences().get(tmp.getId()).removeParticipant(c);
+ }
+ }
+ }
+ }
+ }
+ intent.putExtra("conference", created);
+ intent.putExtra("confID", created.getId());
+ mService.getConferences().put(created.getId(), created);
+ mService.sendBroadcast(intent);
+ }
+
+ @Override
+ public void callOnIncomingMessage(String ID, String from, String msg) {
+ Log.w(TAG, "on_incoming_message:" + msg);
+ Intent intent = new Intent(INCOMING_TEXT);
+ intent.putExtra("CallID", ID);
+ intent.putExtra("From", from);
+ intent.putExtra("Msg", msg);
+
+ if (mService.getConferences().get(ID) != null) {
+ mService.getConferences().get(ID).addSipMessage(new SipMessage(true, msg));
+ intent.putExtra("conference", mService.getConferences().get(ID));
+ } else {
+ for (Map.Entry<String, Conference> stringConferenceEntry : mService.getConferences().entrySet()) {
+ Conference tmp = stringConferenceEntry.getValue();
+ for (SipCall c : tmp.getParticipants()) {
+ if (c.getCallId().contentEquals(ID)) {
+ mService.getConferences().get(tmp.getId()).addSipMessage(new SipMessage(true, msg));
+ intent.putExtra("conference", tmp);
+ }
+ }
+ }
+
+ }
+ mService.sendBroadcast(intent);
+ }
+
+ @Override
+ public void callOnConferenceRemove(String confID) {
+ Log.i(TAG, "on_conference_removed:");
+ Intent intent = new Intent(CONF_REMOVED);
+ intent.putExtra("confID", confID);
+
+ Conference toReInsert = mService.getConferences().get(confID);
+ for (SipCall call : toReInsert.getParticipants()) {
+ mService.getConferences().put(call.getCallId(), new Conference(call));
+ }
+ intent.putExtra("conference", mService.getConferences().get(confID));
+ mService.getConferences().remove(confID);
+ mService.sendBroadcast(intent);
+
+ }
+
+ @Override
+ public void callOnConferenceChanged(String confID, String state) {
+ Log.i(TAG, "on_conference_state_changed:");
+ Intent intent = new Intent(CONF_CHANGED);
+ intent.putExtra("confID", confID);
+ intent.putExtra("State", state);
+
+
+ Log.i(TAG, "Received:" + intent.getAction());
+ Log.i(TAG, "State:" + state);
+
+ Conference toModify = mService.getConferences().get(confID);
+ toModify.setCallState(confID, state);
+
+ ArrayList<String> newParticipants = SwigNativeConverter.convertSwigToNative(Ringservice.sflph_call_get_participant_list(intent.getStringExtra("confID")));
+
+ if (toModify.getParticipants().size() < newParticipants.size()) {
+ // We need to add the new participant to the conf
+ for (String newParticipant : newParticipants) {
+ if (toModify.getCallById(newParticipant) == null) {
+ mService.addCallToConference(toModify.getId(), newParticipant);
+ }
+ }
+ } else if (toModify.getParticipants().size() > newParticipants.size()) {
+ Log.i(TAG, "toModify.getParticipants().size() > newParticipants.size()");
+ for (SipCall participant : toModify.getParticipants()) {
+ if (!newParticipants.contains(participant.getCallId())) {
+ mService.detachCallFromConference(toModify.getId(), participant);
+ break;
+ }
+ }
+ }
+
+ mService.sendBroadcast(intent);
+ }
+
+ @Override
+ public void callOnRecordPlaybackFilepath(String id, String filename) {
+ Intent intent = new Intent();
+ intent.putExtra("callID", id);
+ intent.putExtra("file", filename);
+ mService.sendBroadcast(intent);
+ }
+
+ @Override
+ public void callOnSecureSdesOn(String callID) {
+ Log.i(TAG, "on_secure_sdes_on");
+ SecureSipCall call = (SecureSipCall) mService.getCallById(callID);
+ call.setInitialized();
+ call.useSecureSDES(true);
+ }
+
+ @Override
+ public void callOnSecureSdesOff(String callID) {
+ Log.i(TAG, "on_secure_sdes_off");
+ SecureSipCall call = (SecureSipCall) mService.getCallById(callID);
+ call.setInitialized();
+ call.useSecureSDES(false);
+ }
+
+ @Override
+ public void callOnSecureZrtpOn(String callID, String cipher) {
+ Log.i(TAG, "on_secure_zrtp_on");
+ Intent intent = new Intent(ZRTP_ON);
+ SecureSipCall call = (SecureSipCall) mService.getCallById(callID);
+ call.setInitialized();
+ call.setZrtpSupport(true);
+ intent.putExtra("callID", callID);
+ intent.putExtra("conference", mService.findConference(callID));
+ mService.sendBroadcast(intent);
+ }
+
+ @Override
+ public void callOnSecureZrtpOff(String callID) {
+ Log.i(TAG, "on_secure_zrtp_off");
+ Intent intent = new Intent(ZRTP_OFF);
+ intent.putExtra("callID", callID);
+ SecureSipCall call = (SecureSipCall) mService.getCallById(callID);
+ // Security can be off because call was hung up
+ if (call == null)
+ return;
+
+ call.setInitialized();
+ call.setZrtpSupport(false);
+ intent.putExtra("conference", mService.findConference(callID));
+ mService.sendBroadcast(intent);
+ }
+
+ @Override
+ public void callOnShowSas(String callID, String sas, int verified) {
+ Log.i(TAG, "on_show_sas:" + sas);
+ Intent intent = new Intent(DISPLAY_SAS);
+ SecureSipCall call = (SecureSipCall) mService.getCallById(callID);
+ call.setSAS(sas);
+ call.sasConfirmedByZrtpLayer(verified);
+
+ intent.putExtra("callID", callID);
+ intent.putExtra("SAS", sas);
+ intent.putExtra("verified", verified);
+ intent.putExtra("conference", mService.findConference(callID));
+ mService.sendBroadcast(intent);
+ }
+
+ @Override
+ public void callOnZrtpNotSuppOther(String callID) {
+ Log.i(TAG, "on_zrtp_not_supported");
+ Intent intent = new Intent(ZRTP_NOT_SUPPORTED);
+ SecureSipCall call = (SecureSipCall) mService.getCallById(callID);
+ call.setInitialized();
+ call.setZrtpSupport(false);
+ intent.putExtra("callID", callID);
+ intent.putExtra("conference", mService.findConference(callID));
+ mService.sendBroadcast(intent);
+ }
+
+ @Override
+ public void callOnZrtpNegotiationFail(String callID, String reason, String severity) {
+ Log.i(TAG, "on_zrtp_negociation_failed");
+ Intent intent = new Intent(ZRTP_NEGOTIATION_FAILED);
+ SecureSipCall call = (SecureSipCall) mService.getCallById(callID);
+ call.setInitialized();
+ call.setZrtpSupport(false);
+ intent.putExtra("callID", callID);
+ intent.putExtra("conference", mService.findConference(callID));
+ mService.sendBroadcast(intent);
+ }
+
+ @Override
+ public void callOnRtcpReceiveReport(String callID, IntegerMap stats) {
+ Log.i(TAG, "on_rtcp_report_received");
+ Intent intent = new Intent(RTCP_REPORT_RECEIVED);
+ mService.sendBroadcast(intent);
+ }
+
+}
diff --git a/ring-android/src/cx/ring/service/ConfigurationManagerCallback.java b/ring-android/src/cx/ring/service/ConfigurationManagerCallback.java
new file mode 100644
index 0000000..953a655
--- /dev/null
+++ b/ring-android/src/cx/ring/service/ConfigurationManagerCallback.java
@@ -0,0 +1,125 @@
+/**
+ * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * If you own a pjsip commercial license you can also redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public License
+ * as an android library.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.service;
+
+import android.content.Intent;
+import android.util.Log;
+
+public class ConfigurationManagerCallback extends ConfigurationCallback {
+
+ private SipService mService;
+ private static final String TAG = "ConfigurationManagerCallback";
+
+ static public final String ACCOUNTS_CHANGED = "accounts-changed";
+ static public final String ACCOUNT_STATE_CHANGED = "account-state-changed";
+
+ public ConfigurationManagerCallback(SipService context) {
+ super();
+ mService = context;
+ }
+
+ @Override
+ public void configOnVolumeChange(String device, int value) {
+ super.configOnVolumeChange(device, value);
+ }
+
+ @Override
+ public void configOnAccountsChange() {
+ super.configOnAccountsChange();
+ Intent intent = new Intent(ACCOUNTS_CHANGED);
+ mService.sendBroadcast(intent);
+ }
+
+ @Override
+ public void configOnStunStatusFail(String account_id) {
+ Log.d(TAG, "configOnStunStatusFail : (" + account_id);
+ }
+
+ @Override
+ public void configOnRegistrationStateChange(String accoundID, int state) {
+ String strState = "";
+ switch (state){
+ case 0:
+ strState = "UNREGISTERED";
+ break;
+ case 1:
+ strState = "TRYING";
+ break;
+ case 2:
+ strState = "REGISTERED";
+ break;
+ case 3:
+ strState = "ERROR_GENERIC";
+ break;
+ case 4:
+ strState = "ERROR_AUTH";
+ break;
+ case 5:
+ strState = "ERROR_NETWORK";
+ break;
+ case 6:
+ strState = "ERROR_HOST";
+ break;
+ case 7:
+ strState = "ERROR_EXIST_STUN";
+ break;
+ case 8:
+ strState = "ERROR_NOT_ACCEPTABLE";
+ break;
+ case 9:
+ strState = "NUMBER_OF_STATES";
+ break;
+ }
+
+ sendAccountStateChangedMessage(accoundID, strState, 0);
+ }
+
+ @Override
+ public void configOnSipRegistrationStateChange(String account_id, String state, int code) {
+
+ }
+
+ @Override
+ public void configOnError(int alert) {
+ Log.d(TAG, "configOnError : (" + alert);
+ }
+
+ private void sendAccountStateChangedMessage(String accoundID, String state, int code) {
+ Intent intent = new Intent(ACCOUNT_STATE_CHANGED);
+ intent.putExtra("Account", accoundID);
+ intent.putExtra("state", state);
+ intent.putExtra("code", code);
+ mService.sendBroadcast(intent);
+ }
+
+ public IntVect configGetHardwareAudioFormat(){
+ IntVect result = new IntVect();
+
+ OpenSlParams audioParams = OpenSlParams.createInstance(mService);
+ result.add(audioParams.getSampleRate());
+ result.add(audioParams.getBufferSize());
+
+ return result;
+ }
+
+}
diff --git a/ring-android/src/cx/ring/service/ISipService.aidl b/ring-android/src/cx/ring/service/ISipService.aidl
new file mode 100644
index 0000000..d1ea05a
--- /dev/null
+++ b/ring-android/src/cx/ring/service/ISipService.aidl
@@ -0,0 +1,88 @@
+package cx.ring.service;
+
+import cx.ring.model.SipCall;
+import cx.ring.model.Conference;
+import cx.ring.model.SipMessage;
+
+interface ISipService {
+
+ Map getCallDetails(in String callID);
+ void placeCall(in SipCall call);
+ void refuse(in String callID);
+ void accept(in String callID);
+ void hangUp(in String callID);
+ void hold(in String callID);
+ void unhold(in String callID);
+
+ List getAccountList();
+ String addAccount(in Map accountDetails);
+ void removeAccount(in String accoundId);
+ void setAccountOrder(in String order);
+ Map getAccountDetails(in String accountID);
+ Map getAccountTemplate();
+ void registerAllAccounts();
+ void setAccountDetails(in String accountId, in Map accountDetails);
+ List getCredentials(in String accountID);
+ void setCredentials(in String accountID, in List creds);
+ void setAudioPlugin(in String callID);
+ String getCurrentAudioOutputPlugin();
+ List getAudioCodecList(in String accountID);
+ void setActiveCodecList(in List codecs, in String accountID);
+ Map getRingtoneList();
+
+ boolean checkForPrivateKey(in String pemPath);
+ boolean checkCertificateValidity(in String pemPath);
+ boolean checkHostnameCertificate(in String certificatePath, in String host, in String port);
+
+
+ // FIXME
+ void toggleSpeakerPhone(in boolean toggle);
+
+ /* Recording */
+ void setRecordPath(in String path);
+ String getRecordPath();
+ boolean toggleRecordingCall(in String id);
+ boolean startRecordedFilePlayback(in String filepath);
+ void stopRecordedFilePlayback(in String filepath);
+
+ /* Mute */
+ void setMuted(boolean mute);
+ boolean isCaptureMuted();
+
+ /* Security */
+ void confirmSAS(in String callID);
+ List getTlsSupportedMethods();
+
+ /* DTMF */
+ void playDtmf(in String key);
+
+ /* IM */
+ void sendTextMessage(in String callID, in SipMessage message);
+
+ void transfer(in String callID, in String to);
+ void attendedTransfer(in String transferID, in String targetID);
+
+ /* Conference related methods */
+
+ void removeConference(in String confID);
+ void joinParticipant(in String sel_callID, in String drag_callID);
+
+ void addParticipant(in SipCall call, in String confID);
+ void addMainParticipant(in String confID);
+ void detachParticipant(in String callID);
+ void joinConference(in String sel_confID, in String drag_confID);
+ void hangUpConference(in String confID);
+ void holdConference(in String confID);
+ void unholdConference(in String confID);
+ boolean isConferenceParticipant(in String callID);
+ Map getConferenceList();
+ List getParticipantList(in String confID);
+ String getConferenceId(in String callID);
+ String getConferenceDetails(in String callID);
+
+ Conference getCurrentCall();
+ List getConcurrentCalls();
+
+ Conference getConference(in String id);
+
+}
diff --git a/ring-android/src/cx/ring/service/OpenSlParams.java b/ring-android/src/cx/ring/service/OpenSlParams.java
new file mode 100644
index 0000000..65c73d6
--- /dev/null
+++ b/ring-android/src/cx/ring/service/OpenSlParams.java
@@ -0,0 +1,86 @@
+package cx.ring.service;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Build;
+import android.util.Log;
+
+/**
+ * This class illustrates how to query OpenSL config parameters on Jelly Bean MR1 while maintaining
+ * backward compatibility with older versions of Android. The trick is to place the new API calls in
+ * an inner class that will only be loaded if we're running on JB MR1 or later.
+ */
+public abstract class OpenSlParams {
+
+ /**
+ * @return The recommended sample rate in Hz.
+ */
+ public abstract int getSampleRate();
+
+ /**
+ * @return The recommended buffer size in frames.
+ */
+ public abstract int getBufferSize();
+
+ /**
+ * @param context, e.g., the current activity.
+ * @return OpenSlParams instance for the given context.
+ */
+ public static OpenSlParams createInstance(Context context) {
+ return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ ? new JellyBeanMr1OpenSlParams(context)
+ : new DefaultOpenSlParams();
+ }
+
+ private OpenSlParams() {
+ // Not meant to be instantiated except here.
+ }
+
+ // Implementation for Jelly Bean MR1 or later.
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ private static class JellyBeanMr1OpenSlParams extends OpenSlParams {
+
+ private final int sampleRate;
+ private final int bufferSize;
+
+ private JellyBeanMr1OpenSlParams(Context context) {
+ AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ // Provide default values in case config lookup fails.
+ int sr = 44100;
+ int bs = 64;
+ try {
+ // If possible, query the native sample rate and buffer size.
+ sr = Integer.parseInt(am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE));
+ bs = Integer.parseInt(am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER));
+ } catch (NumberFormatException e) {
+ Log.w(getClass().getName(), "Failed to read native OpenSL config: " + e);
+ }
+ sampleRate = sr;
+ bufferSize = bs;
+ }
+
+ @Override
+ public int getSampleRate() {
+ return sampleRate;
+ }
+
+ @Override
+ public int getBufferSize() {
+ return bufferSize;
+ }
+ }
+
+ // Default factory for Jelly Bean or older.
+ private static class DefaultOpenSlParams extends OpenSlParams {
+ @Override
+ public int getSampleRate() {
+ return 44100;
+ }
+
+ @Override
+ public int getBufferSize() {
+ return 64;
+ }
+ };
+}
diff --git a/ring-android/src/cx/ring/service/ServiceConstants.java b/ring-android/src/cx/ring/service/ServiceConstants.java
new file mode 100644
index 0000000..ab175ab
--- /dev/null
+++ b/ring-android/src/cx/ring/service/ServiceConstants.java
@@ -0,0 +1,15 @@
+package cx.ring.service;
+
+public final class ServiceConstants {
+
+
+ public interface call {
+ public static final String CALL_TYPE = "CALL_TYPE";
+ public static final String PEER_NUMBER = "PEER_NUMBER";
+ public static final String DISPLAY_NAME = "DISPLAY_NAME";
+ public static final String CALL_STATE = "CALL_STATE";
+ public static final String CONF_ID = "CONF_ID";
+ public static final String TIMESTAMP_START = "TIMESTAMP_START";
+ public static final String ACCOUNTID = "ACCOUNTID";
+ }
+}
diff --git a/ring-android/src/cx/ring/service/SipService.java b/ring-android/src/cx/ring/service/SipService.java
new file mode 100644
index 0000000..14d907a
--- /dev/null
+++ b/ring-android/src/cx/ring/service/SipService.java
@@ -0,0 +1,1292 @@
+/**
+ * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Regis Montoya <r3gis.3R@gmail.com>
+ * Author: Emeric Vigier <emeric.vigier@savoirfairelinux.com>
+ * Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * If you own a pjsip commercial license you can also redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public License
+ * as an android library.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.service;
+
+import android.os.Handler;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.*;
+import android.util.Log;
+import cx.ring.history.HistoryManager;
+import cx.ring.model.Codec;
+import cx.ring.model.Conference;
+import cx.ring.model.SecureSipCall;
+import cx.ring.model.SipMessage;
+import cx.ring.utils.MediaManager;
+import cx.ring.utils.SipNotifications;
+import cx.ring.utils.SwigNativeConverter;
+import cx.ring.model.SipCall;
+
+
+public class SipService extends Service {
+
+ static final String TAG = "SipService";
+ private SipServiceExecutor mExecutor;
+ private static HandlerThread executorThread;
+
+ private Handler handler = new Handler();
+ private static int POLLING_TIMEOUT = 500;
+ private Runnable pollEvents = new Runnable() {
+ @Override
+ public void run() {
+ Ringservice.sflph_poll_events();
+ handler.postDelayed(this, POLLING_TIMEOUT);
+ }
+ };
+ private boolean isPjSipStackStarted = false;
+
+ protected SipNotifications mNotificationManager;
+ protected HistoryManager mHistoryManager;
+ protected MediaManager mMediaManager;
+
+ private HashMap<String, Conference> mConferences = new HashMap<String, Conference>();
+ private ConfigurationManagerCallback configurationCallback;
+ private CallManagerCallBack callManagerCallBack;
+
+ public HashMap<String, Conference> getConferences() {
+ return mConferences;
+ }
+
+ public void addCallToConference(String confId, String callId) {
+ if(mConferences.get(callId) != null){
+ // We add a simple call to a conference
+ Log.i(TAG, "// We add a simple call to a conference");
+ mConferences.get(confId).addParticipant(mConferences.get(callId).getParticipants().get(0));
+ mConferences.remove(callId);
+ } else {
+ Log.i(TAG, "addCallToConference");
+ for (Entry<String, Conference> stringConferenceEntry : mConferences.entrySet()) {
+ Conference tmp = stringConferenceEntry.getValue();
+ for (SipCall c : tmp.getParticipants()) {
+ if (c.getCallId().contentEquals(callId)) {
+ mConferences.get(confId).addParticipant(c);
+ mConferences.get(tmp.getId()).removeParticipant(c);
+ }
+ }
+ }
+ }
+ }
+
+ public void detachCallFromConference(String confId, SipCall call) {
+ Log.i(TAG, "detachCallFromConference");
+ Conference separate = new Conference(call);
+ mConferences.put(separate.getId(), separate);
+ mConferences.get(confId).removeParticipant(call);
+ }
+
+ @Override
+ public boolean onUnbind(Intent i) {
+ super.onUnbind(i);
+ Log.i(TAG, "onUnbind(intent)");
+ return true;
+ }
+
+ @Override
+ public void onRebind(Intent i) {
+ super.onRebind(i);
+ }
+
+ /* called once by startService() */
+ @Override
+ public void onCreate() {
+ Log.i(TAG, "onCreated");
+ super.onCreate();
+
+ getExecutor().execute(new StartRunnable());
+
+ mNotificationManager = new SipNotifications(this);
+ mMediaManager = new MediaManager(this);
+ mHistoryManager = new HistoryManager(this);
+
+ mNotificationManager.onServiceCreate();
+ mMediaManager.startService();
+
+ }
+
+ /* called for each startService() */
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.i(TAG, "onStarted");
+ super.onStartCommand(intent, flags, startId);
+ return START_STICKY; /* started and stopped explicitly */
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.i(TAG, "onDestroy");
+ /* called once by stopService() */
+ mNotificationManager.onServiceDestroy();
+ mMediaManager.stopService();
+ getExecutor().execute(new FinalizeRunnable());
+ super.onDestroy();
+
+ }
+
+ @Override
+ public IBinder onBind(Intent arg0) {
+ Log.i(TAG, "onBound");
+ return mBinder;
+ }
+
+ private static Looper createLooper() {
+ if (executorThread == null) {
+ Log.d(TAG, "Creating new handler thread");
+ // ADT gives a fake warning due to bad parse rule.
+ executorThread = new HandlerThread("SipService.Executor");
+ executorThread.start();
+ }
+ return executorThread.getLooper();
+ }
+
+ public SipServiceExecutor getExecutor() {
+ // create mExecutor lazily
+ if (mExecutor == null) {
+ mExecutor = new SipServiceExecutor();
+ }
+ return mExecutor;
+ }
+
+ public SipCall getCallById(String callID) {
+ if (getConferences().get(callID) != null) {
+ return getConferences().get(callID).getCallById(callID);
+ } else {
+ // Check if call is in a conference
+ for (Entry<String, Conference> stringConferenceEntry : getConferences().entrySet()) {
+ Conference tmp = stringConferenceEntry.getValue();
+ SipCall c = tmp.getCallById(callID);
+ if (c != null)
+ return c;
+ }
+ }
+ return null;
+ }
+
+ // Executes immediate tasks in a single executorThread.
+ public static class SipServiceExecutor extends Handler {
+
+ SipServiceExecutor() {
+ super(createLooper());
+ }
+
+ public void execute(Runnable task) {
+ // TODO: add wakelock
+ Message.obtain(SipServiceExecutor.this, 0/* don't care */, task).sendToTarget();
+ Log.w(TAG, "SenT!");
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ Log.w(TAG, "handleMessage");
+ if (msg.obj instanceof Runnable) {
+ executeInternal((Runnable) msg.obj);
+ } else {
+ Log.w(TAG, "can't handle msg: " + msg);
+ }
+ }
+
+ private void executeInternal(Runnable task) {
+ try {
+ task.run();
+ } catch (Throwable t) {
+ Log.e(TAG, "run task: " + task, t);
+ }
+ }
+ }
+
+ private void stopDaemon() {
+ handler.removeCallbacks(pollEvents);
+ Ringservice.sflph_fini();
+ isPjSipStackStarted = false;
+ }
+
+ private void startPjSipStack() throws SameThreadException {
+ if (isPjSipStackStarted)
+ return;
+
+ try {
+ System.loadLibrary("codec_ulaw");
+ System.loadLibrary("codec_alaw");
+ System.loadLibrary("codec_speex");
+ System.loadLibrary("codec_g729");
+ System.loadLibrary("codec_gsm");
+ System.loadLibrary("codec_opus");
+ System.loadLibrary("sflphonejni");
+ isPjSipStackStarted = true;
+
+ } catch (UnsatisfiedLinkError e) {
+ Log.e(TAG, "Problem with the current Pj stack...", e);
+ isPjSipStackStarted = false;
+ return;
+ } catch (Exception e) {
+ Log.e(TAG, "Problem with the current Pj stack...", e);
+ isPjSipStackStarted = false;
+ }
+
+ configurationCallback = new ConfigurationManagerCallback(this);
+ callManagerCallBack = new CallManagerCallBack(this);
+ Ringservice.init(configurationCallback, callManagerCallBack);
+ handler.postDelayed(pollEvents, POLLING_TIMEOUT);
+ Log.i(TAG, "PjSIPStack started");
+ }
+
+ // Enforce same thread contract to ensure we do not call from somewhere else
+ public class SameThreadException extends Exception {
+ private static final long serialVersionUID = -905639124232613768L;
+
+ public SameThreadException() {
+ super("Should be launched from a single worker thread");
+ }
+ }
+
+ public abstract static class SipRunnable implements Runnable {
+ protected abstract void doRun() throws SameThreadException, RemoteException;
+
+ @Override
+ public void run() {
+ try {
+ doRun();
+ } catch (SameThreadException e) {
+ Log.e(TAG, "Not done from same thread");
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ }
+ }
+ }
+
+ public abstract class SipRunnableWithReturn implements Runnable {
+ Object obj = null;
+ boolean done = false;
+
+ protected abstract Object doRun() throws SameThreadException, RemoteException;
+
+ public Object getVal() {
+ return obj;
+ }
+
+ public boolean isDone() {
+ return done;
+ }
+
+ @Override
+ public void run() {
+ try {
+ if (isPjSipStackStarted)
+ obj = doRun();
+ done = true;
+ } catch (SameThreadException e) {
+ Log.e(TAG, "Not done from same thread");
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ }
+ }
+ }
+
+ class StartRunnable extends SipRunnable {
+ @Override
+ protected void doRun() throws SameThreadException {
+ startPjSipStack();
+ }
+ }
+
+ class FinalizeRunnable extends SipRunnable {
+ @Override
+ protected void doRun() throws SameThreadException {
+ stopDaemon();
+ }
+ }
+
+ /* ************************************
+ *
+ * Implement public interface for the service
+ *
+ * *********************************
+ */
+
+ private final ISipService.Stub mBinder = new ISipService.Stub() {
+
+ @Override
+ public void placeCall(final SipCall call) {
+
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.placeCall() thread running...");
+ Conference toAdd;
+ if(call.getAccount().useSecureLayer()){
+ SecureSipCall secureCall = new SecureSipCall(call);
+ toAdd = new Conference(secureCall);
+ } else {
+ toAdd = new Conference(call);
+ }
+ mConferences.put(toAdd.getId(), toAdd);
+ mMediaManager.obtainAudioFocus(false);
+ Ringservice.sflph_call_place(call.getAccount().getAccountID(), call.getCallId(), call.getmContact().getPhones().get(0).getNumber());
+ }
+ });
+ }
+
+ @Override
+ public void refuse(final String callID) {
+
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.refuse() thread running...");
+ Ringservice.sflph_call_refuse(callID);
+ }
+ });
+ }
+
+ @Override
+ public void accept(final String callID) {
+ mMediaManager.stopRing();
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.accept() thread running...");
+ Ringservice.sflph_call_accept(callID);
+ mMediaManager.RouteToInternalSpeaker();
+ }
+ });
+ }
+
+ @Override
+ public void hangUp(final String callID) {
+ mMediaManager.stopRing();
+ Log.e(TAG, "HANGING UP");
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.hangUp() thread running...");
+ Ringservice.sflph_call_hang_up(callID);
+ removeCall(callID);
+ if(mConferences.size() == 0) {
+ Log.i(TAG, "No more calls!");
+ mMediaManager.abandonAudioFocus();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void hold(final String callID) {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.hold() thread running...");
+ Ringservice.sflph_call_hold(callID);
+ }
+ });
+ }
+
+ @Override
+ public void unhold(final String callID) {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.unhold() thread running...");
+ Ringservice.sflph_call_unhold(callID);
+ }
+ });
+ }
+
+ @Override
+ public HashMap<String, String> getCallDetails(String callID) throws RemoteException {
+ class CallDetails extends SipRunnableWithReturn {
+ private String id;
+
+ CallDetails(String callID) {
+ id = callID;
+ }
+
+ @Override
+ protected StringMap doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.getCallDetails() thread running...");
+ return Ringservice.sflph_call_get_call_details(id);
+ }
+ }
+
+ CallDetails runInstance = new CallDetails(callID);
+ getExecutor().execute(runInstance);
+
+ while (!runInstance.isDone()) {
+ }
+ StringMap swigmap = (StringMap) runInstance.getVal();
+
+ return SwigNativeConverter.convertCallDetailsToNative(swigmap);
+ }
+
+ @Override
+ public void setAudioPlugin(final String audioPlugin) {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.setAudioPlugin() thread running...");
+ Ringservice.sflph_config_set_audio_plugin(audioPlugin);
+ }
+ });
+ }
+
+ @Override
+ public String getCurrentAudioOutputPlugin() {
+ class CurrentAudioPlugin extends SipRunnableWithReturn {
+ @Override
+ protected String doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.getCurrentAudioOutputPlugin() thread running...");
+ return Ringservice.sflph_config_get_current_audio_output_plugin();
+ }
+ }
+
+ CurrentAudioPlugin runInstance = new CurrentAudioPlugin();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ // Log.e(TAG, "Waiting for Nofing");
+ }
+ return (String) runInstance.getVal();
+ }
+
+ @Override
+ public ArrayList<String> getAccountList() {
+ class AccountList extends SipRunnableWithReturn {
+ @Override
+ protected StringVect doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.getAccountList() thread running...");
+ return Ringservice.sflph_config_get_account_list();
+ }
+ }
+ AccountList runInstance = new AccountList();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ }
+ StringVect swigvect = (StringVect) runInstance.getVal();
+
+ ArrayList<String> nativelist = new ArrayList<String>();
+
+ for (int i = 0; i < swigvect.size(); i++)
+ nativelist.add(swigvect.get(i));
+
+ return nativelist;
+ }
+
+ @Override
+ public void setAccountOrder(final String order) {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.setAccountsOrder() thread running...");
+ Ringservice.sflph_config_set_accounts_order(order);
+ }
+ });
+ }
+
+ @Override
+ public HashMap<String, String> getAccountDetails(final String accountID) {
+ class AccountDetails extends SipRunnableWithReturn {
+ private String id;
+
+ AccountDetails(String accountId) {
+ id = accountId;
+ }
+
+ @Override
+ protected StringMap doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.getAccountDetails() thread running...");
+ return Ringservice.sflph_config_get_account_details(id);
+ }
+ }
+
+ AccountDetails runInstance = new AccountDetails(accountID);
+ getExecutor().execute(runInstance);
+
+ while (!runInstance.isDone()) {
+ }
+ StringMap swigmap = (StringMap) runInstance.getVal();
+
+ return SwigNativeConverter.convertAccountToNative(swigmap);
+ }
+
+ @SuppressWarnings("unchecked")
+ // Hashmap runtime cast
+ @Override
+ public void setAccountDetails(final String accountId, final Map map) {
+ HashMap<String, String> nativemap = (HashMap<String, String>) map;
+
+ final StringMap swigmap = SwigNativeConverter.convertFromNativeToSwig(nativemap);
+
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException {
+
+ Ringservice.sflph_config_set_account_details(accountId, swigmap);
+ Log.i(TAG, "SipService.setAccountDetails() thread running...");
+ }
+
+ });
+ }
+
+ @Override
+ public Map getAccountTemplate() throws RemoteException {
+ class AccountTemplate extends SipRunnableWithReturn {
+
+ @Override
+ protected StringMap doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.getAccountTemplate() thread running...");
+ return Ringservice.sflph_config_get_account_template();
+ }
+ }
+
+ AccountTemplate runInstance = new AccountTemplate();
+ getExecutor().execute(runInstance);
+
+ while (!runInstance.isDone()) {
+ }
+ StringMap swigmap = (StringMap) runInstance.getVal();
+
+ return SwigNativeConverter.convertAccountToNative(swigmap);
+ }
+
+ @SuppressWarnings("unchecked")
+ // Hashmap runtime cast
+ @Override
+ public String addAccount(Map map) {
+ class AddAccount extends SipRunnableWithReturn {
+ StringMap map;
+
+ AddAccount(StringMap m) {
+ map = m;
+ }
+
+ @Override
+ protected String doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.addAccount() thread running...");
+ return Ringservice.sflph_config_add_account(map);
+ }
+ }
+
+ final StringMap swigmap = SwigNativeConverter.convertFromNativeToSwig((HashMap<String, String>) map);
+
+ AddAccount runInstance = new AddAccount(swigmap);
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ }
+ return (String) runInstance.getVal();
+ }
+
+ @Override
+ public void removeAccount(final String accountId) {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.setAccountDetails() thread running...");
+ Ringservice.sflph_config_remove_account(accountId);
+ }
+ });
+ }
+
+ /*************************
+ * Transfer related API
+ *************************/
+
+ @Override
+ public void transfer(final String callID, final String to) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.transfer() thread running...");
+ if (Ringservice.sflph_call_transfer(callID, to)) {
+ Bundle bundle = new Bundle();
+ bundle.putString("CallID", callID);
+ bundle.putString("State", "HUNGUP");
+ Intent intent = new Intent(CallManagerCallBack.CALL_STATE_CHANGED);
+ intent.putExtra("com.savoirfairelinux.sflphone.service.newstate", bundle);
+ sendBroadcast(intent);
+ } else
+ Log.i(TAG, "NOT OK");
+ }
+ });
+
+ }
+
+ @Override
+ public void attendedTransfer(final String transferID, final String targetID) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.attendedTransfer() thread running...");
+ if (Ringservice.sflph_call_attended_transfer(transferID, targetID)) {
+ Log.i(TAG, "OK");
+ } else
+ Log.i(TAG, "NOT OK");
+ }
+ });
+
+ }
+
+ /*************************
+ * Conference related API
+ *************************/
+
+ @Override
+ public void removeConference(final String confID) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.createConference() thread running...");
+ Ringservice.sflph_call_remove_conference(confID);
+ }
+ });
+
+ }
+
+ @Override
+ public void joinParticipant(final String sel_callID, final String drag_callID) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.joinParticipant() thread running...");
+ Ringservice.sflph_call_join_participant(sel_callID, drag_callID);
+ // Generate a CONF_CREATED callback
+ }
+ });
+ Log.i(TAG, "After joining participants");
+ }
+
+ @Override
+ public void addParticipant(final SipCall call, final String confID) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.addParticipant() thread running...");
+ Ringservice.sflph_call_add_participant(call.getCallId(), confID);
+ mConferences.get(confID).getParticipants().add(call);
+ }
+ });
+
+ }
+
+ @Override
+ public void addMainParticipant(final String confID) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.addMainParticipant() thread running...");
+ Ringservice.sflph_call_add_main_participant(confID);
+ }
+ });
+
+ }
+
+ @Override
+ public void detachParticipant(final String callID) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.detachParticipant() thread running...");
+ Log.i(TAG, "Detaching " + callID);
+ Iterator<Entry<String, Conference>> it = mConferences.entrySet().iterator();
+ Log.i(TAG, "mConferences size " + mConferences.size());
+ while (it.hasNext()) {
+ Conference tmp = it.next().getValue();
+ Log.i(TAG, "conf has " + tmp.getParticipants().size() + " participants");
+ if (tmp.contains(callID)) {
+ Conference toDetach = new Conference(tmp.getCallById(callID));
+ mConferences.put(toDetach.getId(), toDetach);
+ Log.i(TAG, "Call found and put in current_calls");
+ }
+ }
+ Ringservice.sflph_call_detach_participant(callID);
+ }
+ });
+
+ }
+
+ @Override
+ public void joinConference(final String sel_confID, final String drag_confID) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.joinConference() thread running...");
+ Ringservice.sflph_call_join_conference(sel_confID, drag_confID);
+ }
+ });
+
+ }
+
+ @Override
+ public void hangUpConference(final String confID) throws RemoteException {
+ Log.e(TAG, "HANGING UP CONF");
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.joinConference() thread running...");
+ Ringservice.sflph_call_hang_up_conference(confID);
+ }
+ });
+
+ }
+
+ @Override
+ public void holdConference(final String confID) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.holdConference() thread running...");
+ Ringservice.sflph_call_hold_conference(confID);
+ }
+ });
+
+ }
+
+ @Override
+ public void unholdConference(final String confID) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.unholdConference() thread running...");
+ Ringservice.sflph_call_unhold_conference(confID);
+ }
+ });
+
+ }
+
+ @Override
+ public boolean isConferenceParticipant(final String callID) throws RemoteException {
+ class IsParticipant extends SipRunnableWithReturn {
+
+ @Override
+ protected Boolean doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.isRecording() thread running...");
+ return Ringservice.sflph_call_is_conference_participant(callID);
+ }
+ }
+
+ IsParticipant runInstance = new IsParticipant();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ }
+
+ return (Boolean) runInstance.getVal();
+ }
+
+ @Override
+ public HashMap<String, Conference> getConferenceList() throws RemoteException {
+ // class ConfList extends SipRunnableWithReturn {
+ // @Override
+ // protected StringVect doRun() throws SameThreadException {
+ // Log.i(TAG, "SipService.getConferenceList() thread running...");
+ // return callManagerJNI.getConferenceList();
+ // }
+ // }
+ // ;
+ // ConfList runInstance = new ConfList();
+ // getExecutor().execute(runInstance);
+ // while (!runInstance.isDone()) {
+ // // Log.w(TAG, "Waiting for getConferenceList");
+ // }
+ // StringVect swigvect = (StringVect) runInstance.getVal();
+ //
+ // ArrayList<String> nativelist = new ArrayList<String>();
+ //
+ // for (int i = 0; i < swigvect.size(); i++)
+ // nativelist.add(swigvect.get(i));
+ //
+ // return nativelist;
+ return mConferences;
+ }
+
+ @Override
+ public List getParticipantList(final String confID) throws RemoteException {
+ class PartList extends SipRunnableWithReturn {
+ @Override
+ protected StringVect doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.getParticipantList() thread running...");
+ return Ringservice.sflph_call_get_participant_list(confID);
+ }
+ }
+ ;
+ PartList runInstance = new PartList();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ Log.w(TAG, "getParticipantList");
+ }
+ StringVect swigvect = (StringVect) runInstance.getVal();
+ Log.w(TAG, "After that");
+ ArrayList<String> nativelist = new ArrayList<String>();
+
+ for (int i = 0; i < swigvect.size(); i++)
+ nativelist.add(swigvect.get(i));
+
+ return nativelist;
+ }
+
+ @Override
+ public String getConferenceId(String callID) throws RemoteException {
+ Log.e(TAG, "getConferenceList not implemented");
+ return null;
+ }
+
+ @Override
+ public String getConferenceDetails(final String callID) throws RemoteException {
+ class ConfDetails extends SipRunnableWithReturn {
+ @Override
+ protected StringMap doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.getConferenceDetails() thread running...");
+ return Ringservice.sflph_call_get_conference_details(callID);
+ }
+ }
+ ConfDetails runInstance = new ConfDetails();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ // Log.w(TAG, "Waiting for getConferenceList");
+ }
+ StringMap swigvect = (StringMap) runInstance.getVal();
+
+ return swigvect.get("CONF_STATE");
+ }
+
+ @Override
+ public String getRecordPath() throws RemoteException {
+ class RecordPath extends SipRunnableWithReturn {
+
+ @Override
+ protected String doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.getRecordPath() thread running...");
+ return Ringservice.sflph_config_get_record_path();
+ }
+ }
+
+ RecordPath runInstance = new RecordPath();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ // Log.w(TAG, "Waiting for getRecordPath");
+ }
+
+ return (String) runInstance.getVal();
+ }
+
+ @Override
+ public boolean toggleRecordingCall(final String id) throws RemoteException {
+
+ class ToggleRecording extends SipRunnableWithReturn {
+
+ @Override
+ protected Boolean doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.toggleRecordingCall() thread running...");
+ boolean result = Ringservice.sflph_call_toggle_recording(id);
+
+ if (getConferences().containsKey(id)) {
+ getConferences().get(id).setRecording(result);
+ } else {
+ for (Conference c : getConferences().values()) {
+ if (c.getCallById(id) != null)
+ c.getCallById(id).setRecording(result);
+ }
+ }
+ return result;
+ }
+ }
+
+ ToggleRecording runInstance = new ToggleRecording();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ }
+
+ return (Boolean) runInstance.getVal();
+
+ }
+
+ @Override
+ public boolean startRecordedFilePlayback(final String filepath) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.setRecordingCall() thread running...");
+ Ringservice.sflph_call_play_recorded_file(filepath);
+ }
+ });
+ return false;
+ }
+
+ @Override
+ public void stopRecordedFilePlayback(final String filepath) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.stopRecordedFilePlayback() thread running...");
+ Ringservice.sflph_call_stop_recorded_file(filepath);
+ }
+ });
+ }
+
+ @Override
+ public void setRecordPath(final String path) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.setRecordPath() " + path + " thread running...");
+ Ringservice.sflph_config_set_record_path(path);
+ }
+ });
+ }
+
+ @Override
+ public void sendTextMessage(final String callID, final SipMessage message) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.sendTextMessage() thread running...");
+ Ringservice.sflph_call_send_text_message(callID, message.comment);
+ if (getConferences().get(callID) != null)
+ getConferences().get(callID).addSipMessage(message);
+ }
+ });
+
+ }
+
+ @Override
+ public List getAudioCodecList(final String accountID) throws RemoteException {
+ class AudioCodecList extends SipRunnableWithReturn {
+
+ @Override
+ protected ArrayList<Codec> doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.getAudioCodecList() thread running...");
+ ArrayList<Codec> results = new ArrayList<Codec>();
+
+ IntVect active_payloads = Ringservice.sflph_config_get_active_audio_codec_list(accountID);
+ for (int i = 0; i < active_payloads.size(); ++i) {
+
+ results.add(new Codec(active_payloads.get(i), Ringservice.sflph_config_get_audio_codec_details(active_payloads.get(i)), true));
+
+ }
+ IntVect payloads = Ringservice.sflph_config_get_audio_codec_list();
+
+ for (int i = 0; i < payloads.size(); ++i) {
+ boolean isActive = false;
+ for (Codec co : results) {
+ if (co.getPayload().toString().contentEquals(String.valueOf(payloads.get(i))))
+ isActive = true;
+
+ }
+ if (isActive)
+ continue;
+ else
+ results.add(new Codec(payloads.get(i), Ringservice.sflph_config_get_audio_codec_details(payloads.get(i)), false));
+
+ }
+
+ return results;
+ }
+ }
+
+ AudioCodecList runInstance = new AudioCodecList();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ }
+ return (ArrayList<Codec>) runInstance.getVal();
+ }
+
+ @Override
+ public Map getRingtoneList() throws RemoteException {
+ class RingtoneList extends SipRunnableWithReturn {
+
+ @Override
+ protected StringMap doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.getRingtoneList() thread running...");
+ return Ringservice.sflph_config_get_ringtone_list();
+ }
+ }
+
+ RingtoneList runInstance = new RingtoneList();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ }
+ StringMap ringtones = (StringMap) runInstance.getVal();
+
+ for (int i = 0; i < ringtones.size(); ++i) {
+ // Log.i(TAG,"ringtones "+i+" "+ ringtones.);
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean checkForPrivateKey(final String pemPath) throws RemoteException {
+ class hasPrivateKey extends SipRunnableWithReturn {
+
+ @Override
+ protected Boolean doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.isCaptureMuted() thread running...");
+ return Ringservice.sflph_config_check_for_private_key(pemPath);
+ }
+ }
+
+ hasPrivateKey runInstance = new hasPrivateKey();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ }
+
+ return (Boolean) runInstance.getVal();
+ }
+
+ @Override
+ public boolean checkCertificateValidity(final String pemPath) throws RemoteException {
+ class isValid extends SipRunnableWithReturn {
+
+ @Override
+ protected Boolean doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.isCaptureMuted() thread running...");
+ return Ringservice.sflph_config_check_certificate_validity(pemPath, pemPath);
+ }
+ }
+
+ isValid runInstance = new isValid();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ }
+
+ return (Boolean) runInstance.getVal();
+ }
+
+ @Override
+ public boolean checkHostnameCertificate(final String certificatePath, final String host, final String port) throws RemoteException {
+ class isValid extends SipRunnableWithReturn {
+
+ @Override
+ protected Boolean doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.isCaptureMuted() thread running...");
+ return Ringservice.sflph_config_check_hostname_certificate(host, port);
+ }
+ }
+
+ isValid runInstance = new isValid();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ }
+
+ return (Boolean) runInstance.getVal();
+ }
+
+ @Override
+ public void setActiveCodecList(final List codecs, final String accountID) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.setActiveAudioCodecList() thread running...");
+ StringVect list = new StringVect();
+ for (Object codec : codecs) {
+ list.add((String) codec);
+ }
+ Ringservice.sflph_config_set_active_audio_codec_list(list, accountID);
+ }
+ });
+ }
+
+
+ @Override
+ public Conference getCurrentCall() throws RemoteException {
+ for (Conference conf : mConferences.values()) {
+ if (conf.isIncoming())
+ return conf;
+ }
+
+ for (Conference conf : mConferences.values()) {
+ if (conf.isOnGoing())
+ return conf;
+ }
+
+ return null;
+ }
+
+ @Override
+ public void playDtmf(final String key) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.playDtmf() thread running...");
+ Ringservice.sflph_call_play_dtmf(key);
+ }
+ });
+ }
+
+ @Override
+ public List getConcurrentCalls() throws RemoteException {
+ return new ArrayList(mConferences.values());
+ }
+
+ @Override
+ public Conference getConference(String id) throws RemoteException {
+ return mConferences.get(id);
+ }
+
+ @Override
+ public void setMuted(final boolean mute) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.setMuted() thread running...");
+ Ringservice.sflph_config_mute_capture(mute);
+ }
+ });
+ }
+
+ @Override
+ public boolean isCaptureMuted() throws RemoteException {
+ class IsMuted extends SipRunnableWithReturn {
+
+ @Override
+ protected Boolean doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.isCaptureMuted() thread running...");
+ return Ringservice.sflph_config_is_capture_muted();
+ }
+ }
+
+ IsMuted runInstance = new IsMuted();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ }
+
+ return (Boolean) runInstance.getVal();
+ }
+
+ @Override
+ public void confirmSAS(final String callID) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.confirmSAS() thread running...");
+ SecureSipCall call = (SecureSipCall) getCallById(callID);
+ call.setSASConfirmed(true);
+ Ringservice.sflph_call_set_sas_verified(callID);
+ }
+ });
+ }
+
+
+ @Override
+ public List getTlsSupportedMethods(){
+ class TlsMethods extends SipRunnableWithReturn {
+
+ @Override
+ protected List doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.getCredentials() thread running...");
+ StringVect map = Ringservice.sflph_config_get_supported_tls_method();
+ return SwigNativeConverter.convertSwigToNative(map);
+ }
+ }
+
+ TlsMethods runInstance = new TlsMethods();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ }
+ return (List) runInstance.getVal();
+ }
+
+ @Override
+ public List getCredentials(final String accountID) throws RemoteException {
+ class Credentials extends SipRunnableWithReturn {
+
+ @Override
+ protected List doRun() throws SameThreadException {
+ Log.i(TAG, "SipService.getCredentials() thread running...");
+ VectMap map = Ringservice.sflph_config_get_credentials(accountID);
+ return SwigNativeConverter.convertCredentialsToNative(map);
+ }
+ }
+
+ Credentials runInstance = new Credentials();
+ getExecutor().execute(runInstance);
+ while (!runInstance.isDone()) {
+ }
+ return (List) runInstance.getVal();
+ }
+
+ @Override
+ public void setCredentials(final String accountID, final List creds) throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.setCredentials() thread running...");
+ ArrayList<HashMap<String, String>> list = (ArrayList<HashMap<String, String>>) creds;
+ Ringservice.sflph_config_set_credentials(accountID, SwigNativeConverter.convertFromNativeToSwig(creds));
+ }
+ });
+ }
+
+ @Override
+ public void registerAllAccounts() throws RemoteException {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ Log.i(TAG, "SipService.registerAllAccounts() thread running...");
+ Ringservice.sflph_config_register_all_accounts();
+ }
+ });
+ }
+
+ @Override
+ public void toggleSpeakerPhone(boolean toggle) throws RemoteException {
+ if (toggle)
+ mMediaManager.RouteToSpeaker();
+ else
+ mMediaManager.RouteToInternalSpeaker();
+ }
+
+ };
+
+ private void removeCall(String callID) {
+ Conference conf = findConference(callID);
+ if(conf == null)
+ return;
+ if(conf.getParticipants().size() == 1)
+ getConferences().remove(conf.getId());
+ else
+ conf.removeParticipant(conf.getCallById(callID));
+ }
+
+ protected Conference findConference(String callID) {
+ Conference result = null;
+ if (getConferences().get(callID) != null) {
+ result = getConferences().get(callID);
+ } else {
+ for (Entry<String, Conference> stringConferenceEntry : getConferences().entrySet()) {
+ Conference tmp = stringConferenceEntry.getValue();
+ for (SipCall c : tmp.getParticipants()) {
+ if (c.getCallId().contentEquals(callID)) {
+ result = tmp;
+ }
+ }
+ }
+ }
+ return result;
+ }
+}
diff --git a/ring-android/src/cx/ring/utils/AccelerometerListener.java b/ring-android/src/cx/ring/utils/AccelerometerListener.java
new file mode 100644
index 0000000..f2bbca7
--- /dev/null
+++ b/ring-android/src/cx/ring/utils/AccelerometerListener.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Regis Montoya <r3gis.3R@gmail.com>
+ * Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.utils;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.Message;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * This class is used to listen to the accelerometer to monitor the orientation of the phone. The client of this class is notified when the
+ * orientation changes between horizontal and vertical.
+ */
+public final class AccelerometerListener {
+ private SensorManager mSensorManager;
+ private Sensor mSensor;
+
+ // mOrientation is the orientation value most recently reported to the client.
+ private int mOrientation;
+
+ // mPendingOrientation is the latest orientation computed based on the sensor value.
+ // This is sent to the client after a rebounce delay, at which point it is copied to
+ // mOrientation.
+ private int mPendingOrientation;
+
+ private OrientationListener mListener;
+
+ // Device orientation
+ public static final int ORIENTATION_UNKNOWN = 0;
+ public static final int ORIENTATION_VERTICAL = 1;
+ public static final int ORIENTATION_HORIZONTAL = 2;
+
+ private static final int ORIENTATION_CHANGED = 1234;
+
+ private static final int VERTICAL_DEBOUNCE = 100;
+ private static final int HORIZONTAL_DEBOUNCE = 500;
+ private static final double VERTICAL_ANGLE = 50.0;
+
+ public interface OrientationListener {
+ public void orientationChanged(int orientation);
+ }
+
+ public AccelerometerListener(Context context, OrientationListener listener) {
+ mListener = listener;
+ mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+ mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ }
+
+ public void enable(boolean enable) {
+// if (DEBUG)
+// Log.d(TAG, "enable(" + enable + ")");
+ synchronized (this) {
+ if (enable) {
+ mOrientation = ORIENTATION_UNKNOWN;
+ mPendingOrientation = ORIENTATION_UNKNOWN;
+ mSensorManager.registerListener(mSensorListener, mSensor, SensorManager.SENSOR_DELAY_NORMAL);
+ } else {
+ mSensorManager.unregisterListener(mSensorListener);
+ mHandler.removeMessages(ORIENTATION_CHANGED);
+ }
+ }
+ }
+
+ private void setOrientation(int orientation) {
+ synchronized (this) {
+ if (mPendingOrientation == orientation) {
+ // Pending orientation has not changed, so do nothing.
+ return;
+ }
+
+ // Cancel any pending messages.
+ // We will either start a new timer or cancel alltogether
+ // if the orientation has not changed.
+ mHandler.removeMessages(ORIENTATION_CHANGED);
+
+ if (mOrientation != orientation) {
+ // Set timer to send an event if the orientation has changed since its
+ // previously reported value.
+ mPendingOrientation = orientation;
+ Message m = mHandler.obtainMessage(ORIENTATION_CHANGED);
+ // set delay to our debounce timeout
+ int delay = (orientation == ORIENTATION_VERTICAL ? VERTICAL_DEBOUNCE : HORIZONTAL_DEBOUNCE);
+ mHandler.sendMessageDelayed(m, delay);
+ } else {
+ // no message is pending
+ mPendingOrientation = ORIENTATION_UNKNOWN;
+ }
+ }
+ }
+
+ private void onSensorEvent(double x, double y, double z) {
+// if (VDEBUG)
+// Log.d(TAG, "onSensorEvent(" + x + ", " + y + ", " + z + ")");
+
+ // If some values are exactly zero, then likely the sensor is not powered up yet.
+ // ignore these events to avoid false horizontal positives.
+ if (x == 0.0 || y == 0.0 || z == 0.0)
+ return;
+
+ // magnitude of the acceleration vector projected onto XY plane
+ double xy = Math.sqrt(x * x + y * y);
+ // compute the vertical angle
+ double angle = Math.atan2(xy, z);
+ // convert to degrees
+ angle = angle * 180.0 / Math.PI;
+ int orientation = (angle > VERTICAL_ANGLE ? ORIENTATION_VERTICAL : ORIENTATION_HORIZONTAL);
+// if (VDEBUG)
+// Log.d(TAG, "angle: " + angle + " orientation: " + orientation);
+ setOrientation(orientation);
+ }
+
+ SensorEventListener mSensorListener = new SensorEventListener() {
+ public void onSensorChanged(SensorEvent event) {
+ onSensorEvent(event.values[0], event.values[1], event.values[2]);
+ }
+
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ // ignore
+ }
+ };
+
+ Handler mHandler = new AccelerometerHandler(this);
+
+ private static class AccelerometerHandler extends Handler {
+ WeakReference<AccelerometerListener> l;
+
+ AccelerometerHandler(AccelerometerListener listener) {
+ l = new WeakReference<AccelerometerListener>(listener);
+ }
+
+ public void handleMessage(Message msg) {
+ AccelerometerListener listener = l.get();
+ if (listener == null) {
+ return;
+ }
+ switch (msg.what) {
+ case ORIENTATION_CHANGED:
+ synchronized (listener) {
+ listener.mOrientation = listener.mPendingOrientation;
+// if (DEBUG) {
+// Log.d(TAG, "orientation: "
+// + (listener.mOrientation == ORIENTATION_HORIZONTAL ? "horizontal"
+// : (listener.mOrientation == ORIENTATION_VERTICAL ? "vertical" : "unknown")));
+// }
+ listener.mListener.orientationChanged(listener.mOrientation);
+ }
+ break;
+ }
+ }
+ };
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/utils/CallProximityManager.java b/ring-android/src/cx/ring/utils/CallProximityManager.java
new file mode 100644
index 0000000..509aa42
--- /dev/null
+++ b/ring-android/src/cx/ring/utils/CallProximityManager.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Regis Montoya <r3gis.3R@gmail.com>
+ * Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+package cx.ring.utils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import cx.ring.utils.AccelerometerListener.OrientationListener;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.net.wifi.WifiManager;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+
+/**
+ * Class to manage proximity detection while in call.
+ *
+ */
+public class CallProximityManager implements SensorEventListener, OrientationListener {
+ private static final String THIS_FILE = "CallProximityManager";
+
+ private Context mContext;
+
+ private SensorManager sensorManager;
+ private PowerManager powerManager;
+
+ // Timeout management of screen locker ui
+ // private ScreenLocker mScreenLocker;
+ private Boolean useTimeoutOverlay = null;
+
+ // Self management of proximity sensor
+ private Sensor proximitySensor;
+ private static final float PROXIMITY_THRESHOLD = 5.0f;
+ private boolean invertProximitySensor = false;
+ private boolean proximitySensorTracked = false;
+ private boolean isFirstRun = true;
+ private ProximityDirector mDirector = null;
+
+ // The hidden api that uses a wake lock
+ private WakeLock proximityWakeLock;
+
+ // The accelerometer
+ private AccelerometerListener accelerometerManager;
+ private int mOrientation;
+ private boolean accelerometerEnabled = false;
+
+ private int WAIT_FOR_PROXIMITY_NEGATIVE = 1;
+ // private final static int SCREEN_LOCKER_ACQUIRE_DELAY = "google_sdk".equals(Build.PRODUCT) ? ScreenLocker.WAIT_BEFORE_LOCK_LONG
+ // : ScreenLocker.WAIT_BEFORE_LOCK_SHORT;
+
+ private static Method powerLockReleaseIntMethod;
+
+ public interface ProximityDirector {
+ public boolean shouldActivateProximity();
+
+ public void onProximityTrackingChanged(boolean acquired);
+ }
+
+ public CallProximityManager(Context context, ProximityDirector director) {
+ mContext = context;
+ mDirector = director;
+ // mScreenLocker = screenLocker;
+
+ sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+ powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ accelerometerManager = new AccelerometerListener(context, this);
+
+ // Try to detect the hidden api
+ if (powerManager != null) {
+ WifiManager wman = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+
+ // Try to use powermanager proximity sensor
+ try {
+ boolean supportProximity = false;
+ Field f = PowerManager.class.getDeclaredField("PROXIMITY_SCREEN_OFF_WAKE_LOCK");
+ int proximityScreenOffWakeLock = (Integer) f.get(null);
+ if (Compatibility.isCompatible(17)) {
+ // Changes of the private API on android 4.2
+ Method method = powerManager.getClass().getDeclaredMethod("isWakeLockLevelSupported", int.class);
+ supportProximity = (Boolean) method.invoke(powerManager, proximityScreenOffWakeLock);
+ Log.d(THIS_FILE, "Use 4.2 detection way for proximity sensor detection. Result is " + supportProximity);
+ } else {
+ Method method = powerManager.getClass().getDeclaredMethod("getSupportedWakeLockFlags");
+ int supportedFlags = (Integer) method.invoke(powerManager);
+ Log.d(THIS_FILE, "Proxmity flags supported : " + supportedFlags);
+ supportProximity = ((supportedFlags & proximityScreenOffWakeLock) != 0x0);
+ }
+ if (supportProximity) {
+ Log.d(THIS_FILE, "We can use native screen locker !!");
+ proximityWakeLock = powerManager.newWakeLock(proximityScreenOffWakeLock, "org.sflphone.CallProximity");
+ proximityWakeLock.setReferenceCounted(false);
+ }
+
+ } catch (Exception e) {
+ Log.d(THIS_FILE, "Impossible to get power manager supported wake lock flags ");
+ }
+ if (powerLockReleaseIntMethod == null) {
+ try {
+ powerLockReleaseIntMethod = proximityWakeLock.getClass().getDeclaredMethod("release", int.class);
+
+ } catch (Exception e) {
+ Log.d(THIS_FILE, "Impossible to get power manager release with it");
+ }
+
+ }
+ }
+
+ // Try to detect a proximity sensor as fallback
+ if (proximityWakeLock == null) {
+ proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+ // invertProximitySensor = SipConfigManager.getPreferenceBooleanValue(context, SipConfigManager.INVERT_PROXIMITY_SENSOR);
+ }
+
+ }
+
+ public synchronized void startTracking() {
+ // If we should manage it ourselves
+ if (proximitySensor != null && !proximitySensorTracked) {
+ // Fall back to manual mode
+ isFirstRun = true;
+ Log.d(THIS_FILE, "Register sensor");
+ sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
+ proximitySensorTracked = true;
+ }
+ if (!accelerometerEnabled) {
+ accelerometerManager.enable(true);
+ accelerometerEnabled = true;
+ }
+ }
+
+ public synchronized void stopTracking() {
+ if (proximitySensor != null && proximitySensorTracked) {
+ proximitySensorTracked = false;
+ sensorManager.unregisterListener(this);
+ Log.d(THIS_FILE, "Unregister to sensor is done !!!");
+ }
+ if (accelerometerEnabled) {
+ accelerometerManager.enable(false);
+ accelerometerEnabled = false;
+ }
+ // mScreenLocker.tearDown();
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ // Log.d(THIS_FILE, "Tracked : "+proximitySensorTracked);
+ if (proximitySensorTracked && !isFirstRun) {
+ float distance = event.values[0];
+ boolean active = (distance >= 0.0 && distance < PROXIMITY_THRESHOLD && distance < event.sensor.getMaximumRange());
+ if (invertProximitySensor) {
+ active = !active;
+ }
+ Log.d(THIS_FILE, "Distance is now " + distance);
+
+ boolean isValidCallState = false;
+ if (mDirector != null) {
+ isValidCallState = mDirector.shouldActivateProximity();
+ }
+
+ if (isValidCallState && active) {
+ // mScreenLocker.show();
+ if (mDirector != null) {
+ mDirector.onProximityTrackingChanged(true);
+ }
+ } else {
+ // mScreenLocker.hide();
+ if (mDirector != null) {
+ mDirector.onProximityTrackingChanged(false);
+ }
+ }
+
+ }
+ if (isFirstRun) {
+ isFirstRun = false;
+ }
+ }
+
+ private boolean isProximityWakeHeld = false;
+
+ /**
+ * Release any lock taken by the proximity sensor
+ */
+ public synchronized void release(int flag) {
+ if (proximityWakeLock != null && isProximityWakeHeld) {
+ boolean usedNewRelease = false;
+ if (powerLockReleaseIntMethod != null) {
+ try {
+ powerLockReleaseIntMethod.invoke(proximityWakeLock, flag);
+ usedNewRelease = true;
+ // Log.d(THIS_FILE, "CALL NEW RELEASE WITH FLAG " + flag);
+ } catch (Exception e) {
+ Log.d(THIS_FILE, "Error calling new release method ", e);
+ }
+ }
+ if (!usedNewRelease) {
+ proximityWakeLock.release();
+ }
+ isProximityWakeHeld = false;
+ }
+
+ if (shouldUseTimeoutOverlay()) {
+ // mScreenLocker.hide();
+ }
+ // Notify
+ if (mDirector != null) {
+ mDirector.onProximityTrackingChanged(false);
+ }
+ }
+
+ public synchronized void acquire() {
+ if (proximityWakeLock != null && !isProximityWakeHeld) {
+ proximityWakeLock.acquire();
+ isProximityWakeHeld = true;
+ }
+ if (shouldUseTimeoutOverlay()) {
+ // mScreenLocker.delayedLock(SCREEN_LOCKER_ACQUIRE_DELAY);
+ }
+ // Notify
+ if (mDirector != null) {
+ mDirector.onProximityTrackingChanged(true);
+ }
+ }
+
+ /**
+ * Update proximity lock mode depending on current state
+ */
+ public synchronized void updateProximitySensorMode() {
+
+ // We do not keep the screen off when the user is outside in-call screen and we are
+ // horizontal, but we do not force it on when we become horizontal until the
+ // proximity sensor goes negative.
+ boolean horizontal = (mOrientation == AccelerometerListener.ORIENTATION_HORIZONTAL);
+
+ boolean activeRegardingCalls = false;
+ if (mDirector != null) {
+ activeRegardingCalls = mDirector.shouldActivateProximity();
+ }
+
+ //Log.d(THIS_FILE, "Horizontal : " + horizontal + " and activate for calls " + activeRegardingCalls);
+ if (activeRegardingCalls && !horizontal) {
+ // Phone is in use! Arrange for the screen to turn off
+ // automatically when the sensor detects a close object.
+ acquire();
+ } else {
+ // Phone is either idle, or ringing. We don't want any
+ // special proximity sensor behavior in either case.
+ int flags = (!horizontal ? 0 : WAIT_FOR_PROXIMITY_NEGATIVE);
+ release(flags);
+ }
+ }
+
+ /**
+ * Should the application display the overlay after a timeout.
+ *
+ * @return false if we are in table mode or if proximity sensor can be used
+ */
+ private boolean shouldUseTimeoutOverlay() {
+ if (useTimeoutOverlay == null) {
+ useTimeoutOverlay = proximitySensor == null && proximityWakeLock == null && !Compatibility.isTabletScreen(mContext);
+ }
+ return useTimeoutOverlay;
+ }
+
+ public void restartTimer() {
+ if (shouldUseTimeoutOverlay()) {
+ // mScreenLocker.delayedLock(ScreenLocker.WAIT_BEFORE_LOCK_LONG);
+ }
+ }
+
+ @Override
+ public void orientationChanged(int orientation) {
+ mOrientation = orientation;
+ updateProximitySensorMode();
+ }
+
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/utils/Compatibility.java b/ring-android/src/cx/ring/utils/Compatibility.java
new file mode 100644
index 0000000..b40cbfc
--- /dev/null
+++ b/ring-android/src/cx/ring/utils/Compatibility.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Regis Montoya <r3gis.3R@gmail.com>
+ * Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.utils;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.lang.reflect.Field;
+import java.util.regex.Pattern;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
+import android.media.AudioManager;
+import android.media.MediaRecorder.AudioSource;
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Log;
+
+@SuppressWarnings("deprecation")
+public final class Compatibility {
+
+ private Compatibility() {
+ }
+
+ private static final String THIS_FILE = "Compat";
+
+ public static int getApiLevel() {
+ return android.os.Build.VERSION.SDK_INT;
+ }
+
+ public static boolean isCompatible(int apiLevel) {
+ return android.os.Build.VERSION.SDK_INT >= apiLevel;
+ }
+
+ /**
+ * Get the stream id for in call track. Can differ on some devices. Current device for which it's different :
+ *
+ * @return
+ */
+ public static int getInCallStream(boolean requestBluetooth) {
+ /* Archos 5IT */
+ if (android.os.Build.BRAND.equalsIgnoreCase("archos") && android.os.Build.DEVICE.equalsIgnoreCase("g7a")) {
+ // Since archos has no voice call capabilities, voice call stream is
+ // not implemented
+ // So we have to choose the good stream tag, which is by default
+ // falled back to music
+ return AudioManager.STREAM_MUSIC;
+ }
+ if (requestBluetooth) {
+ return 6; /* STREAM_BLUETOOTH_SCO -- Thx @Stefan for the contrib */
+ }
+
+ // return AudioManager.STREAM_MUSIC;
+ return AudioManager.STREAM_VOICE_CALL;
+ }
+
+ public static boolean shouldUseRoutingApi() {
+ Log.d(THIS_FILE, "Current device " + android.os.Build.BRAND + " - " + android.os.Build.DEVICE);
+
+ // HTC evo 4G
+ if (android.os.Build.PRODUCT.equalsIgnoreCase("htc_supersonic")) {
+ return true;
+ }
+
+ // ZTE joe
+ if (android.os.Build.DEVICE.equalsIgnoreCase("joe")) {
+ return true;
+ }
+
+ // Samsung GT-S5830
+ return android.os.Build.DEVICE.toUpperCase().startsWith("GT-S");
+ }
+
+ public static boolean shouldUseModeApi() {
+
+ // ZTE blade et joe
+ if (android.os.Build.DEVICE.equalsIgnoreCase("blade") || android.os.Build.DEVICE.equalsIgnoreCase("joe")) {
+ return true;
+ }
+ // Samsung GT-S5360 GT-S5830 GT-S6102 ... probably all..
+ if (android.os.Build.DEVICE.toUpperCase().startsWith("GT-") || android.os.Build.PRODUCT.toUpperCase().startsWith("GT-")
+ || android.os.Build.DEVICE.toUpperCase().startsWith("YP-")) {
+ return true;
+ }
+
+ // HTC evo 4G
+ if (android.os.Build.PRODUCT.equalsIgnoreCase("htc_supersonic")) {
+ return true;
+ }
+ // LG P500, Optimus V
+ if (android.os.Build.DEVICE.toLowerCase().startsWith("thunder")) {
+ return true;
+ }
+ // LG-E720(b)
+ if (android.os.Build.MODEL.toUpperCase().startsWith("LG-E720") && !Compatibility.isCompatible(9)) {
+ return true;
+ }
+ // LG-LS840
+ if (android.os.Build.DEVICE.toLowerCase().startsWith("cayman")) {
+ return true;
+ }
+
+ // Huawei
+ if (android.os.Build.DEVICE.equalsIgnoreCase("U8150") || android.os.Build.DEVICE.equalsIgnoreCase("U8110")
+ || android.os.Build.DEVICE.equalsIgnoreCase("U8120") || android.os.Build.DEVICE.equalsIgnoreCase("U8100")
+ || android.os.Build.PRODUCT.equalsIgnoreCase("U8655")) {
+ return true;
+ }
+
+ // Moto defy mini
+ if (android.os.Build.MODEL.equalsIgnoreCase("XT320")) {
+ return true;
+ }
+
+ // Alcatel
+ if (android.os.Build.DEVICE.toUpperCase().startsWith("ONE_TOUCH_993D")) {
+ return true;
+ }
+
+ // N4
+ return android.os.Build.DEVICE.toUpperCase().startsWith("MAKO");
+
+ }
+
+ public static String guessInCallMode() {
+ // New api for 2.3.3 is not available on galaxy S II :(
+ if (!isCompatible(11) && android.os.Build.DEVICE.toUpperCase().startsWith("GT-I9100")) {
+ return Integer.toString(AudioManager.MODE_NORMAL);
+ }
+
+ if (android.os.Build.BRAND.equalsIgnoreCase("sdg") || isCompatible(10)) {
+ // Note that in APIs this is only available from level 11.
+ return "3";
+ }
+ if (android.os.Build.DEVICE.equalsIgnoreCase("blade")) {
+ return Integer.toString(AudioManager.MODE_IN_CALL);
+ }
+
+ if (!isCompatible(5)) {
+ return Integer.toString(AudioManager.MODE_IN_CALL);
+ }
+
+ return Integer.toString(AudioManager.MODE_NORMAL);
+ }
+
+ public static String getDefaultMicroSource() {
+ // Except for galaxy S II :(
+ if (!isCompatible(11) && android.os.Build.DEVICE.toUpperCase().startsWith("GT-I9100")) {
+ return Integer.toString(AudioSource.MIC);
+ }
+
+ if (isCompatible(10)) {
+ // Note that in APIs this is only available from level 11.
+ // VOICE_COMMUNICATION
+ return Integer.toString(0x7);
+ }
+ /*
+ * Too risky in terms of regressions else if (isCompatible(4)) { // VOICE_CALL return 0x4; }
+ */
+ /*
+ * if(android.os.Build.MODEL.equalsIgnoreCase("X10i")) { // VOICE_CALL return Integer.toString(0x4); }
+ */
+ /*
+ * Not relevant anymore, atrix I tested sounds fine with that if(android.os.Build.DEVICE.equalsIgnoreCase("olympus")) { //Motorola atrix bug
+ * // CAMCORDER return Integer.toString(0x5); }
+ */
+
+ return Integer.toString(AudioSource.DEFAULT);
+ }
+
+ public static String getDefaultFrequency() {
+ if (android.os.Build.DEVICE.equalsIgnoreCase("olympus")) {
+ // Atrix bug
+ return "32000";
+ }
+ if (android.os.Build.DEVICE.toUpperCase().equals("GT-P1010")) {
+ // Galaxy tab see issue 932
+ return "32000";
+ }
+
+ return isCompatible(4) ? "16000" : "8000";
+ }
+
+ public static String getCpuAbi() {
+ if (isCompatible(4)) {
+ Field field;
+ try {
+ field = android.os.Build.class.getField("CPU_ABI");
+ return field.get(null).toString();
+ } catch (Exception e) {
+ Log.w(THIS_FILE, "Announce to be android 1.6 but no CPU ABI field", e);
+ }
+
+ }
+ return "armeabi";
+ }
+
+ public final static int getNumCores() {
+ // Private Class to display only CPU devices in the directory listing
+ class CpuFilter implements FileFilter {
+ @Override
+ public boolean accept(File pathname) {
+ // Check if filename is "cpu", followed by a single digit number
+ if (Pattern.matches("cpu[0-9]", pathname.getName())) {
+ return true;
+ }
+ return false;
+ }
+ }
+ try {
+ // Get directory containing CPU info
+ File dir = new File("/sys/devices/system/cpu/");
+ // Filter to only list the devices we care about
+ File[] files = dir.listFiles(new CpuFilter());
+ // Return the number of cores (virtual CPU devices)
+ return files.length;
+ } catch (Exception e) {
+ return Runtime.getRuntime().availableProcessors();
+ }
+ }
+
+ private static boolean needPspWorkaround() {
+ // New api for 2.3 does not work on Incredible S
+ if (android.os.Build.DEVICE.equalsIgnoreCase("vivo")) {
+ return true;
+ }
+
+ // New API for android 2.3 should be able to manage this but do only for
+ // honeycomb cause seems not correctly supported by all yet
+ if (isCompatible(11)) {
+ return false;
+ }
+
+ // All htc except....
+ if (android.os.Build.PRODUCT.toLowerCase().startsWith("htc") || android.os.Build.BRAND.toLowerCase().startsWith("htc")
+ || android.os.Build.PRODUCT.toLowerCase().equalsIgnoreCase("inc") /*
+ * For Incredible
+ */
+ || android.os.Build.DEVICE.equalsIgnoreCase("passion") /* N1 */) {
+ if (android.os.Build.DEVICE.equalsIgnoreCase("hero") /* HTC HERO */
+ || android.os.Build.DEVICE.equalsIgnoreCase("magic") /*
+ * Magic Aka Dev G2
+ */
+ || android.os.Build.DEVICE.equalsIgnoreCase("tatoo") /* Tatoo */
+ || android.os.Build.DEVICE.equalsIgnoreCase("dream") /*
+ * Dream Aka Dev G1
+ */
+ || android.os.Build.DEVICE.equalsIgnoreCase("legend") /* Legend */
+
+ ) {
+ return false;
+ }
+
+ // Older than 2.3 has no chance to have the new full perf wifi mode
+ // working since does not exists
+ if (!isCompatible(9)) {
+ return true;
+ } else {
+ // N1 is fine with that
+ if (android.os.Build.DEVICE.equalsIgnoreCase("passion")) {
+ return false;
+ }
+ return true;
+ }
+
+ }
+ // Dell streak
+ if (android.os.Build.BRAND.toLowerCase().startsWith("dell") && android.os.Build.DEVICE.equalsIgnoreCase("streak")) {
+ return true;
+ }
+ // Motorola milestone 1 and 2 & motorola droid & defy not under 2.3
+ if ((android.os.Build.DEVICE.toLowerCase().contains("milestone2") || android.os.Build.BOARD.toLowerCase().contains("sholes")
+ || android.os.Build.PRODUCT.toLowerCase().contains("sholes") || android.os.Build.DEVICE.equalsIgnoreCase("olympus") || android.os.Build.DEVICE
+ .toLowerCase().contains("umts_jordan")) && !isCompatible(9)) {
+ return true;
+ }
+ // Moto defy mini
+ if (android.os.Build.MODEL.equalsIgnoreCase("XT320")) {
+ return true;
+ }
+
+ // Alcatel ONE touch
+ if (android.os.Build.DEVICE.startsWith("one_touch_990")) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static boolean needToneWorkaround() {
+ if (android.os.Build.PRODUCT.toLowerCase().startsWith("gt-i5800") || android.os.Build.PRODUCT.toLowerCase().startsWith("gt-i5801")
+ || android.os.Build.PRODUCT.toLowerCase().startsWith("gt-i9003")) {
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean needSGSWorkaround() {
+ if (isCompatible(9)) {
+ return false;
+ }
+ if (android.os.Build.DEVICE.toUpperCase().startsWith("GT-I9000") || android.os.Build.DEVICE.toUpperCase().startsWith("GT-P1000")) {
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean needWebRTCImplementation() {
+ if (android.os.Build.DEVICE.toLowerCase().contains("droid2")) {
+ return true;
+ }
+ if (android.os.Build.MODEL.toLowerCase().contains("droid bionic")) {
+ return true;
+ }
+ if (android.os.Build.DEVICE.toLowerCase().contains("sunfire")) {
+ return true;
+ }
+ // Huawei Y300
+ if (android.os.Build.DEVICE.equalsIgnoreCase("U8833")) {
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean shouldSetupAudioBeforeInit() {
+ // Setup for GT / GS samsung devices.
+ if (android.os.Build.DEVICE.toLowerCase().startsWith("gt-") || android.os.Build.PRODUCT.toLowerCase().startsWith("gt-")) {
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean shouldFocusAudio() {
+ /* HTC One X */
+ if (android.os.Build.DEVICE.toLowerCase().startsWith("endeavoru") || android.os.Build.DEVICE.toLowerCase().startsWith("evita")) {
+ return false;
+ }
+
+ if (android.os.Build.DEVICE.toUpperCase().startsWith("GT-P7510") && isCompatible(15)) {
+ return false;
+ }
+ return true;
+ }
+
+ // private static int getDefaultAudioImplementation() {
+ // // Acer A510
+ // if (android.os.Build.DEVICE.toLowerCase().startsWith("picasso")) {
+ // return SipConfigManager.AUDIO_IMPLEMENTATION_JAVA;
+ // }
+ // if (Compatibility.isCompatible(11)) {
+ // return SipConfigManager.AUDIO_IMPLEMENTATION_OPENSLES;
+ // }
+ // if (android.os.Build.DEVICE.equalsIgnoreCase("ST25i") && Compatibility.isCompatible(10)) {
+ // return SipConfigManager.AUDIO_IMPLEMENTATION_OPENSLES;
+ // }
+ // if (android.os.Build.DEVICE.equalsIgnoreCase("u8510") && Compatibility.isCompatible(10)) {
+ // return SipConfigManager.AUDIO_IMPLEMENTATION_OPENSLES;
+ // }
+ // return SipConfigManager.AUDIO_IMPLEMENTATION_JAVA;
+ // }
+
+ public static boolean useFlipAnimation() {
+ if (android.os.Build.BRAND.equalsIgnoreCase("archos") && android.os.Build.DEVICE.equalsIgnoreCase("g7a")) {
+ return false;
+ }
+ return true;
+ }
+
+ public static Intent getContactPhoneIntent() {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ /*
+ * intent.setAction(Intent.ACTION_GET_CONTENT); intent.setType(Contacts.Phones.CONTENT_ITEM_TYPE);
+ */
+ // Don't use constant to allow backward compat simply
+ intent.setData(Uri.parse("content://com.android.contacts/contacts"));
+ return intent;
+ }
+
+ public static boolean isTabletScreen(Context ctxt) {
+ boolean isTablet = false;
+ if (!isCompatible(4)) {
+ return false;
+ }
+ Configuration cfg = ctxt.getResources().getConfiguration();
+ int screenLayoutVal = 0;
+ try {
+ Field f = Configuration.class.getDeclaredField("screenLayout");
+ screenLayoutVal = (Integer) f.get(cfg);
+ } catch (Exception e) {
+ return false;
+ }
+ int screenLayout = (screenLayoutVal & 0xF);
+ // 0xF = SCREENLAYOUT_SIZE_MASK but avoid 1.5 incompat doing that
+ if (screenLayout == 0x3 || screenLayout == 0x4) {
+ // 0x3 = SCREENLAYOUT_SIZE_LARGE but avoid 1.5 incompat doing that
+ // 0x4 = SCREENLAYOUT_SIZE_XLARGE but avoid 1.5 incompat doing that
+ isTablet = true;
+ }
+
+ return isTablet;
+ }
+
+ public static int getHomeMenuId() {
+ return 0x0102002c;
+ // return android.R.id.home;
+ }
+
+ public static boolean isInstalledOnSdCard(Context context) {
+ // check for API level 8 and higher
+ if (Compatibility.isCompatible(8)) {
+ PackageManager pm = context.getPackageManager();
+ try {
+ PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
+ ApplicationInfo ai = pi.applicationInfo;
+ return (ai.flags & 0x00040000 /*
+ * ApplicationInfo. FLAG_EXTERNAL_STORAGE
+ */) == 0x00040000 /*
+ * ApplicationInfo. FLAG_EXTERNAL_STORAGE
+ */;
+ } catch (NameNotFoundException e) {
+ // ignore
+ }
+ }
+
+ // check for API level 7 - check files dir
+ try {
+ String filesDir = context.getFilesDir().getAbsolutePath();
+ if (filesDir.startsWith("/data/")) {
+ return false;
+ } else if (filesDir.contains(Environment.getExternalStorageDirectory().getPath())) {
+ return true;
+ }
+ } catch (Throwable e) {
+ // ignore
+ }
+
+ return false;
+ }
+
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/utils/MediaManager.java b/ring-android/src/cx/ring/utils/MediaManager.java
new file mode 100644
index 0000000..e1abe4e
--- /dev/null
+++ b/ring-android/src/cx/ring/utils/MediaManager.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.utils;
+
+import cx.ring.service.SipService;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.os.Handler;
+import android.util.Log;
+import cx.ring.utils.bluetooth.BluetoothWrapper;
+
+public class MediaManager implements OnAudioFocusChangeListener, BluetoothWrapper.BluetoothChangeListener {
+
+ private static final String TAG = MediaManager.class.getSimpleName();
+ private SipService mService;
+ private SettingsContentObserver mSettingsContentObserver;
+ AudioManager mAudioManager;
+ private Ringer ringer;
+ //Bluetooth related
+ private BluetoothWrapper bluetoothWrapper;
+
+ public MediaManager(SipService aService) {
+ mService = aService;
+ mSettingsContentObserver = new SettingsContentObserver(mService, new Handler());
+ mAudioManager = (AudioManager) aService.getSystemService(Context.AUDIO_SERVICE);
+
+ ringer = new Ringer(aService);
+ }
+
+ public void startService() {
+ if(bluetoothWrapper == null) {
+ bluetoothWrapper = BluetoothWrapper.getInstance(mService);
+ bluetoothWrapper.setBluetoothChangeListener(this);
+ bluetoothWrapper.register();
+ }
+ mService.getApplicationContext().getContentResolver()
+ .registerContentObserver(android.provider.Settings.System.CONTENT_URI, true, mSettingsContentObserver);
+ }
+
+ public void stopService() {
+ Log.i(TAG, "Remove media manager....");
+ mService.getApplicationContext().getContentResolver().unregisterContentObserver(mSettingsContentObserver);
+ if(bluetoothWrapper != null) {
+ bluetoothWrapper.unregister();
+ bluetoothWrapper.setBluetoothChangeListener(null);
+ bluetoothWrapper = null;
+ }
+ }
+
+ public AudioManager getAudioManager() {
+ return mAudioManager;
+ }
+
+ public void obtainAudioFocus(boolean requestSpeakerOn) {
+ mAudioManager.requestAudioFocus(this, Compatibility.getInCallStream(false), AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+ if(bluetoothWrapper != null && bluetoothWrapper.canBluetooth()) {
+ Log.d(TAG, "Try to enable bluetooth");
+ bluetoothWrapper.setBluetoothOn(true);
+ } else if (requestSpeakerOn && !mAudioManager.isWiredHeadsetOn()){
+ RouteToSpeaker();
+ }
+ }
+
+ @Override
+ public void onAudioFocusChange(int arg0) {
+
+ }
+
+ public void abandonAudioFocus() {
+ mAudioManager.abandonAudioFocus(this);
+ if (mAudioManager.isSpeakerphoneOn()) {
+ mAudioManager.setSpeakerphoneOn(false);
+ }
+ mAudioManager.setMode(AudioManager.MODE_NORMAL);
+ }
+
+ public void RouteToSpeaker() {
+ mAudioManager.setSpeakerphoneOn(true);
+ }
+
+ public void RouteToInternalSpeaker() {
+ mAudioManager.setSpeakerphoneOn(false);
+ }
+
+
+ /**
+ * Start ringing announce for a given contact.
+ * It will also focus audio for us.
+ * @param remoteContact the contact to ring for. May resolve the contact ringtone if any.
+ */
+ synchronized public void startRing(String remoteContact) {
+
+ if(!ringer.isRinging()) {
+ ringer.ring(remoteContact, "USELESS");
+ }else {
+ Log.d(TAG, "Already ringing ....");
+ }
+
+ }
+
+ /**
+ * Stop all ringing. <br/>
+ * Warning, this will not unfocus audio.
+ */
+ synchronized public void stopRing() {
+ if(ringer.isRinging()) {
+ ringer.stopRing();
+ }
+ }
+
+ @Override
+ public void onBluetoothStateChanged(int status) {
+ //setSoftwareVolume();
+ //broadcastMediaChanged();
+ }
+
+}
diff --git a/ring-android/src/cx/ring/utils/Ringer.java b/ring-android/src/cx/ring/utils/Ringer.java
new file mode 100644
index 0000000..1e2767d
--- /dev/null
+++ b/ring-android/src/cx/ring/utils/Ringer.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.utils;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Vibrator;
+import android.util.Log;
+
+
+/**
+ * Ringer manager for the Phone app.
+ */
+public class Ringer {
+ private static final String THIS_FILE = "Ringer";
+
+ private static final int VIBRATE_LENGTH = 1000; // ms
+ private static final int PAUSE_LENGTH = 1000; // ms
+
+ // Uri for the ringtone.
+ Uri customRingtoneUri;
+
+ Vibrator vibrator;
+ VibratorThread vibratorThread;
+ Context context;
+
+ public Ringer(Context aContext) {
+ context = aContext;
+ vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ }
+
+ /**
+ * Starts the ringtone and/or vibrator.
+ *
+ */
+ public void ring(String remoteContact, String defaultRingtone) {
+ Log.d(THIS_FILE, "==> ring() called...");
+
+ synchronized (this) {
+
+ AudioManager audioManager =
+ (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+
+ //Save ringtone at the begining in case we raise vol
+// ringtone = getRingtone(remoteContact, defaultRingtone);
+
+ //No ring no vibrate
+ int ringerMode = audioManager.getRingerMode();
+ if (ringerMode == AudioManager.RINGER_MODE_SILENT) {
+ Log.d(THIS_FILE, "skipping ring and vibrate because profile is Silent");
+ return;
+ }
+
+ // Vibrate
+ int vibrateSetting = audioManager.getVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER);
+ Log.d(THIS_FILE, "v=" + vibrateSetting + " rm=" + ringerMode);
+ if (vibratorThread == null &&
+ (vibrateSetting == AudioManager.VIBRATE_SETTING_ON ||
+ ringerMode == AudioManager.RINGER_MODE_VIBRATE)) {
+ vibratorThread = new VibratorThread();
+ Log.d(THIS_FILE, "Starting vibrator...");
+ vibratorThread.start();
+ }
+
+ // Vibrate only
+ if (ringerMode == AudioManager.RINGER_MODE_VIBRATE ||
+ audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0 ) {
+ Log.d(THIS_FILE, "skipping ring because profile is Vibrate OR because volume is zero");
+ return;
+ }
+
+ }
+ }
+
+ /**
+ * @return true if we're playing a ringtone and/or vibrating
+ * to indicate that there's an incoming call.
+ * ("Ringing" here is used in the general sense. If you literally
+ * need to know if we're playing a ringtone or vibrating, use
+ * isRingtonePlaying() or isVibrating() instead.)
+ */
+ public boolean isRinging() {
+ return (vibratorThread != null);
+ }
+
+ /**
+ * Stops the ringtone and/or vibrator if any of these are actually
+ * ringing/vibrating.
+ */
+ public void stopRing() {
+ synchronized (this) {
+ Log.d(THIS_FILE, "==> stopRing() called...");
+
+ stopVibrator();
+ }
+ }
+
+
+ private void stopVibrator() {
+
+ if (vibratorThread != null) {
+ vibratorThread.interrupt();
+ try {
+ vibratorThread.join(250); // Should be plenty long (typ.)
+ } catch (InterruptedException e) {
+ } // Best efforts (typ.)
+ vibratorThread = null;
+ }
+ }
+
+ public void updateRingerMode() {
+
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ synchronized (this) {
+ int ringerMode = audioManager.getRingerMode();
+ // Silent : stop everything
+ if (ringerMode == AudioManager.RINGER_MODE_SILENT) {
+ stopRing();
+ return;
+ }
+
+ // Vibrate
+ int vibrateSetting = audioManager.getVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER);
+ // If not already started restart it
+ if (vibratorThread == null && (vibrateSetting == AudioManager.VIBRATE_SETTING_ON || ringerMode == AudioManager.RINGER_MODE_VIBRATE)) {
+ vibratorThread = new VibratorThread();
+ vibratorThread.start();
+ }
+
+ // Vibrate only
+ if (ringerMode == AudioManager.RINGER_MODE_VIBRATE || audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) {
+ return;
+ }
+
+ }
+ }
+
+ private class VibratorThread extends Thread {
+ public void run() {
+ try {
+ while (true) {
+ vibrator.vibrate(VIBRATE_LENGTH);
+ Thread.sleep(VIBRATE_LENGTH + PAUSE_LENGTH);
+ }
+ } catch (InterruptedException ex) {
+ Log.d(THIS_FILE, "Vibrator thread interrupt");
+ } finally {
+ vibrator.cancel();
+ }
+ Log.d(THIS_FILE, "Vibrator thread exiting");
+ }
+ }
+
+}
diff --git a/ring-android/src/cx/ring/utils/SettingsContentObserver.java b/ring-android/src/cx/ring/utils/SettingsContentObserver.java
new file mode 100644
index 0000000..018cf28
--- /dev/null
+++ b/ring-android/src/cx/ring/utils/SettingsContentObserver.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.utils;
+
+import cx.ring.service.SipService;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.media.AudioManager;
+import android.os.Handler;
+import android.util.Log;
+
+public class SettingsContentObserver extends ContentObserver {
+ double previousVolume;
+ SipService context;
+ private static final String TAG = "Settings";
+
+ public SettingsContentObserver(SipService c, Handler handler) {
+ super(handler);
+ context=c;
+ AudioManager audio = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ previousVolume = audio.getStreamVolume(AudioManager.STREAM_VOICE_CALL);
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return super.deliverSelfNotifications();
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+
+ AudioManager audio = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ double currentVolume = audio.getStreamVolume(AudioManager.STREAM_VOICE_CALL);
+
+ double delta=previousVolume-currentVolume;
+
+ if(delta>0)
+ {
+ Log.d(TAG,"Decreased");
+ previousVolume=currentVolume;
+// context.changeVolume(currentVolume);
+ }
+ else if(delta<0)
+ {
+ Log.d(TAG,"Increased");
+ previousVolume=currentVolume;
+// context.changeVolume(currentVolume);
+ }
+ }
+}
diff --git a/ring-android/src/cx/ring/utils/SipNotifications.java b/ring-android/src/cx/ring/utils/SipNotifications.java
new file mode 100644
index 0000000..356552b
--- /dev/null
+++ b/ring-android/src/cx/ring/utils/SipNotifications.java
@@ -0,0 +1,203 @@
+/**
+ * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ * Adrien Béraud <adrien.beraud@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.utils;
+
+import java.util.HashMap;
+import java.util.Random;
+
+import cx.ring.R;
+import cx.ring.client.HomeActivity;
+import cx.ring.model.Conference;
+import cx.ring.model.SipCall;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.graphics.Typeface;
+import android.net.sip.SipProfile;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationCompat.Builder;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
+
+public class SipNotifications {
+
+ private final NotificationManager notificationManager;
+ private final Context context;
+
+ public static final String NOTIF_CREATION = "notif_creation";
+ public static final String NOTIF_DELETION = "notif_deletion";
+
+ private final int NOTIFICATION_ID = new Random().nextInt(1000);
+
+ public static final int REGISTER_NOTIF_ID = 1;
+ public static final int CALL_NOTIF_ID = REGISTER_NOTIF_ID + 1;
+ public static final int CALLLOG_NOTIF_ID = REGISTER_NOTIF_ID + 2;
+ public static final int MESSAGE_NOTIF_ID = REGISTER_NOTIF_ID + 3;
+ public static final int VOICEMAIL_NOTIF_ID = REGISTER_NOTIF_ID + 4;
+
+ private static boolean isInit = false;
+
+ public SipNotifications(Context aContext) {
+ context = aContext;
+ notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ if (!isInit) {
+ cancelAll();
+ cancelCalls();
+ isInit = true;
+ }
+
+ }
+
+ public void onServiceCreate() {
+
+ }
+
+ public void onServiceDestroy() {
+ // Make sure our notification is gone.
+ cancelAll();
+ cancelCalls();
+ }
+
+ // Calls
+ public void showNotificationForCall(SipCall callInfo) {
+ // TODO
+ }
+
+ public void showNotificationForVoiceMail(SipProfile acc, int numberOfMessages) {
+ // TODO
+ }
+
+ protected static CharSequence buildTickerMessage(Context context, String address, String body) {
+ String displayAddress = address;
+
+ StringBuilder buf = new StringBuilder(displayAddress == null ? "" : displayAddress.replace('\n', ' ').replace('\r', ' '));
+ buf.append(':').append(' ');
+
+ int offset = buf.length();
+
+ if (!TextUtils.isEmpty(body)) {
+ body = body.replace('\n', ' ').replace('\r', ' ');
+ buf.append(body);
+ }
+
+ SpannableString spanText = new SpannableString(buf.toString());
+ spanText.setSpan(new StyleSpan(Typeface.BOLD), 0, offset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ return spanText;
+ }
+
+ public final void cancelCalls() {
+ notificationManager.cancel(CALL_NOTIF_ID);
+ }
+
+ public final void cancelMissedCalls() {
+ notificationManager.cancel(CALLLOG_NOTIF_ID);
+ }
+
+ public final void cancelMessages() {
+ notificationManager.cancel(MESSAGE_NOTIF_ID);
+ }
+
+ public final void cancelVoicemails() {
+ notificationManager.cancel(VOICEMAIL_NOTIF_ID);
+ }
+
+ public final void cancelAll() {
+ cancelMessages();
+ cancelMissedCalls();
+ cancelVoicemails();
+ }
+
+ public void publishMissedCallNotification(Conference missedConf) {
+
+ CharSequence tickerText = context.getString(R.string.notif_missed_call_title);
+ long when = System.currentTimeMillis();
+
+ Builder nb = new NotificationCompat.Builder(context);
+ nb.setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher));
+ nb.setSmallIcon(R.drawable.ic_action_call);
+
+ nb.setTicker(tickerText);
+ nb.setWhen(when);
+ nb.setContentTitle(context.getString(R.string.notif_missed_call_title));
+ nb.setContentText(context.getString(R.string.notif_missed_call_content, missedConf.getParticipants().get(0).getmContact().getmDisplayName()));
+ Intent notificationIntent = new Intent(context, HomeActivity.class);
+ notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ // notification.setLatestEventInfo(context, contentTitle,
+ // contentText, contentIntent);
+ nb.setOnlyAlertOnce(true);
+ nb.setContentIntent(contentIntent);
+
+ Notification notification = nb.build();
+ // We have to re-write content view because getNotification setLatestEventInfo implicitly
+ // notification.contentView = contentView;
+
+ // startForegroundCompat(CALL_NOTIF_ID, notification);
+ notificationManager.notify(CALL_NOTIF_ID, notification);
+ }
+
+ public void makeNotification(HashMap<String, SipCall> calls) {
+ if (calls.size() == 0) {
+ return;
+ }
+ Intent notificationIntent = new Intent(context, HomeActivity.class);
+ PendingIntent contentIntent = PendingIntent.getActivity(context, 007, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancel(NOTIFICATION_ID); // clear previous notifications.
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
+
+ builder.setContentIntent(contentIntent).setOngoing(true).setSmallIcon(R.drawable.ic_launcher)
+ .setContentTitle(calls.size() + " ongoing calls").setTicker("Pending calls").setWhen(System.currentTimeMillis()).setAutoCancel(false);
+ builder.setPriority(NotificationCompat.PRIORITY_MAX);
+ Notification n = builder.build();
+
+ nm.notify(NOTIFICATION_ID, n);
+ }
+
+ public void removeNotification() {
+ NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancel(NOTIFICATION_ID);
+ }
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/utils/SwigNativeConverter.java b/ring-android/src/cx/ring/utils/SwigNativeConverter.java
new file mode 100644
index 0000000..748391b
--- /dev/null
+++ b/ring-android/src/cx/ring/utils/SwigNativeConverter.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.utils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+
+import cx.ring.model.account.AccountDetailAdvanced;
+import cx.ring.model.account.AccountDetailBasic;
+import cx.ring.model.account.AccountDetailSrtp;
+import cx.ring.model.account.AccountDetailTls;
+import cx.ring.service.ServiceConstants;
+import cx.ring.service.StringMap;
+import cx.ring.service.StringVect;
+import cx.ring.service.VectMap;
+
+public class SwigNativeConverter {
+
+ /**
+ * Native to Swig
+ */
+
+ public static StringMap convertFromNativeToSwig(HashMap<String, String> nativemap) {
+ StringMap swigmap = new StringMap();
+
+ Set<String> keys = nativemap.keySet();
+ for (String key : keys) {
+ if (nativemap.get(key) == null) {
+ swigmap.set(key, "");
+ } else {
+ swigmap.set(key, nativemap.get(key));
+ }
+ }
+ return swigmap;
+ }
+
+ public static VectMap convertFromNativeToSwig(List creds) {
+ ArrayList<HashMap<String, String>> todecode = (ArrayList<HashMap<String, String>>) creds;
+ VectMap toReturn = new VectMap();
+
+ for (HashMap<String, String> aTodecode : todecode) {
+ StringMap entry = new StringMap();
+ entry.set(AccountDetailBasic.CONFIG_ACCOUNT_PASSWORD, aTodecode.get(AccountDetailBasic.CONFIG_ACCOUNT_PASSWORD));
+ entry.set(AccountDetailBasic.CONFIG_ACCOUNT_USERNAME, aTodecode.get(AccountDetailBasic.CONFIG_ACCOUNT_USERNAME));
+ entry.set(AccountDetailBasic.CONFIG_ACCOUNT_REALM, aTodecode.get(AccountDetailBasic.CONFIG_ACCOUNT_REALM));
+ toReturn.add(entry);
+ }
+ return toReturn;
+ }
+
+ private static String tryToGet(StringMap smap, String key) {
+ if (smap.has_key(key)) {
+ return smap.get(key);
+ } else {
+ return "";
+ }
+ }
+
+ public static HashMap<String, String> convertAccountToNative(StringMap swigmap) {
+ HashMap<String, String> nativemap = new HashMap<String, String>();
+
+ nativemap.put(AccountDetailBasic.CONFIG_ACCOUNT_ALIAS, swigmap.get(AccountDetailBasic.CONFIG_ACCOUNT_ALIAS));
+ nativemap.put(AccountDetailBasic.CONFIG_ACCOUNT_HOSTNAME, swigmap.get(AccountDetailBasic.CONFIG_ACCOUNT_HOSTNAME));
+ nativemap.put(AccountDetailBasic.CONFIG_ACCOUNT_USERNAME, swigmap.get(AccountDetailBasic.CONFIG_ACCOUNT_USERNAME));
+ nativemap.put(AccountDetailBasic.CONFIG_ACCOUNT_PASSWORD, swigmap.get(AccountDetailBasic.CONFIG_ACCOUNT_PASSWORD));
+ nativemap.put(AccountDetailBasic.CONFIG_ACCOUNT_TYPE, swigmap.get(AccountDetailBasic.CONFIG_ACCOUNT_TYPE));
+ nativemap.put(AccountDetailBasic.CONFIG_ACCOUNT_ENABLE, swigmap.get(AccountDetailBasic.CONFIG_ACCOUNT_ENABLE));
+ nativemap.put(AccountDetailBasic.CONFIG_ACCOUNT_USERAGENT, swigmap.get(AccountDetailBasic.CONFIG_ACCOUNT_USERAGENT));
+ nativemap.put(AccountDetailAdvanced.CONFIG_ACCOUNT_MAILBOX, swigmap.get(AccountDetailAdvanced.CONFIG_ACCOUNT_MAILBOX));
+ nativemap.put(AccountDetailBasic.CONFIG_ACCOUNT_AUTOANSWER, swigmap.get(AccountDetailBasic.CONFIG_ACCOUNT_AUTOANSWER));
+
+ if (swigmap.get(AccountDetailBasic.CONFIG_ACCOUNT_TYPE).equals("SIP"))
+ {
+
+ nativemap.put(AccountDetailBasic.CONFIG_ACCOUNT_ROUTESET, swigmap.get(AccountDetailBasic.CONFIG_ACCOUNT_ROUTESET));
+ nativemap
+ .put(AccountDetailAdvanced.CONFIG_ACCOUNT_REGISTRATION_EXPIRE, swigmap.get(AccountDetailAdvanced.CONFIG_ACCOUNT_REGISTRATION_EXPIRE));
+ nativemap.put(AccountDetailAdvanced.CONFIG_LOCAL_INTERFACE, swigmap.get(AccountDetailAdvanced.CONFIG_LOCAL_INTERFACE));
+ nativemap.put(AccountDetailAdvanced.CONFIG_STUN_SERVER, swigmap.get(AccountDetailAdvanced.CONFIG_STUN_SERVER));
+ nativemap
+ .put(AccountDetailAdvanced.CONFIG_ACCOUNT_REGISTRATION_STATUS, swigmap.get(AccountDetailAdvanced.CONFIG_ACCOUNT_REGISTRATION_STATUS));
+ nativemap.put(AccountDetailAdvanced.CONFIG_ACCOUNT_REGISTRATION_STATE_CODE,
+ swigmap.get(AccountDetailAdvanced.CONFIG_ACCOUNT_REGISTRATION_STATE_CODE));
+ nativemap.put(AccountDetailAdvanced.CONFIG_ACCOUNT_REGISTRATION_STATE_DESC,
+ swigmap.get(AccountDetailAdvanced.CONFIG_ACCOUNT_REGISTRATION_STATE_DESC));
+ nativemap.put(AccountDetailAdvanced.CONFIG_ACCOUNT_DTMF_TYPE, swigmap.get(AccountDetailAdvanced.CONFIG_ACCOUNT_DTMF_TYPE));
+ nativemap.put(AccountDetailAdvanced.CONFIG_KEEP_ALIVE_ENABLED, swigmap.get(AccountDetailAdvanced.CONFIG_KEEP_ALIVE_ENABLED));
+ nativemap.put(AccountDetailAdvanced.CONFIG_LOCAL_PORT, swigmap.get(AccountDetailAdvanced.CONFIG_LOCAL_PORT));
+ nativemap.put(AccountDetailAdvanced.CONFIG_PUBLISHED_ADDRESS, swigmap.get(AccountDetailAdvanced.CONFIG_PUBLISHED_ADDRESS));
+ nativemap.put(AccountDetailAdvanced.CONFIG_PUBLISHED_PORT, swigmap.get(AccountDetailAdvanced.CONFIG_PUBLISHED_PORT));
+ nativemap.put(AccountDetailAdvanced.CONFIG_PUBLISHED_SAMEAS_LOCAL, swigmap.get(AccountDetailAdvanced.CONFIG_PUBLISHED_SAMEAS_LOCAL));
+ nativemap.put(AccountDetailAdvanced.CONFIG_RINGTONE_ENABLED, swigmap.get(AccountDetailAdvanced.CONFIG_RINGTONE_ENABLED));
+ nativemap.put(AccountDetailAdvanced.CONFIG_RINGTONE_PATH, swigmap.get(AccountDetailAdvanced.CONFIG_RINGTONE_PATH));
+ nativemap.put(AccountDetailAdvanced.CONFIG_STUN_ENABLE, swigmap.get(AccountDetailAdvanced.CONFIG_STUN_ENABLE));
+ nativemap.put(AccountDetailAdvanced.CONFIG_AUDIO_PORT_MAX, swigmap.get(AccountDetailAdvanced.CONFIG_AUDIO_PORT_MAX));
+ nativemap.put(AccountDetailAdvanced.CONFIG_AUDIO_PORT_MIN, swigmap.get(AccountDetailAdvanced.CONFIG_AUDIO_PORT_MIN));
+
+ nativemap.put(AccountDetailSrtp.CONFIG_SRTP_KEY_EXCHANGE, swigmap.get(AccountDetailSrtp.CONFIG_SRTP_KEY_EXCHANGE));
+ nativemap.put(AccountDetailSrtp.CONFIG_SRTP_RTP_FALLBACK, swigmap.get(AccountDetailSrtp.CONFIG_SRTP_RTP_FALLBACK));
+ nativemap.put(AccountDetailSrtp.CONFIG_ZRTP_DISPLAY_SAS, swigmap.get(AccountDetailSrtp.CONFIG_ZRTP_DISPLAY_SAS));
+ nativemap.put(AccountDetailSrtp.CONFIG_ZRTP_DISPLAY_SAS_ONCE, swigmap.get(AccountDetailSrtp.CONFIG_ZRTP_DISPLAY_SAS_ONCE));
+ nativemap.put(AccountDetailSrtp.CONFIG_ZRTP_HELLO_HASH, swigmap.get(AccountDetailSrtp.CONFIG_ZRTP_HELLO_HASH));
+ nativemap.put(AccountDetailSrtp.CONFIG_ZRTP_NOT_SUPP_WARNING, swigmap.get(AccountDetailSrtp.CONFIG_ZRTP_NOT_SUPP_WARNING));
+ nativemap.put(AccountDetailSrtp.CONFIG_SRTP_ENABLE, swigmap.get(AccountDetailSrtp.CONFIG_SRTP_ENABLE));
+
+ nativemap.put(AccountDetailTls.CONFIG_TLS_CIPHERS, swigmap.get(AccountDetailTls.CONFIG_TLS_CIPHERS));
+ nativemap.put(AccountDetailTls.CONFIG_TLS_LISTENER_PORT, swigmap.get(AccountDetailTls.CONFIG_TLS_LISTENER_PORT));
+ nativemap.put(AccountDetailTls.CONFIG_TLS_METHOD, swigmap.get(AccountDetailTls.CONFIG_TLS_METHOD));
+ nativemap.put(AccountDetailTls.CONFIG_TLS_NEGOTIATION_TIMEOUT_SEC, swigmap.get(AccountDetailTls.CONFIG_TLS_NEGOTIATION_TIMEOUT_SEC));
+ nativemap.put(AccountDetailTls.CONFIG_TLS_PASSWORD, swigmap.get(AccountDetailTls.CONFIG_TLS_PASSWORD));
+ nativemap.put(AccountDetailTls.CONFIG_TLS_PRIVATE_KEY_FILE, swigmap.get(AccountDetailTls.CONFIG_TLS_PRIVATE_KEY_FILE));
+ nativemap.put(AccountDetailTls.CONFIG_TLS_REQUIRE_CLIENT_CERTIFICATE, swigmap.get(AccountDetailTls.CONFIG_TLS_REQUIRE_CLIENT_CERTIFICATE));
+ nativemap.put(AccountDetailTls.CONFIG_TLS_SERVER_NAME, swigmap.get(AccountDetailTls.CONFIG_TLS_SERVER_NAME));
+ nativemap.put(AccountDetailTls.CONFIG_TLS_VERIFY_CLIENT, swigmap.get(AccountDetailTls.CONFIG_TLS_VERIFY_CLIENT));
+ nativemap.put(AccountDetailTls.CONFIG_TLS_VERIFY_SERVER, swigmap.get(AccountDetailTls.CONFIG_TLS_VERIFY_SERVER));
+ nativemap.put(AccountDetailTls.CONFIG_TLS_CERTIFICATE_FILE, swigmap.get(AccountDetailTls.CONFIG_TLS_CERTIFICATE_FILE));
+ nativemap.put(AccountDetailTls.CONFIG_TLS_CA_LIST_FILE, swigmap.get(AccountDetailTls.CONFIG_TLS_CA_LIST_FILE));
+ nativemap.put(AccountDetailTls.CONFIG_TLS_ENABLE, swigmap.get(AccountDetailTls.CONFIG_TLS_ENABLE));
+ }
+
+ return nativemap;
+ }
+
+ public static HashMap<String, String> convertCallDetailsToNative(StringMap swigmap) {
+
+ HashMap<String, String> entry = new HashMap<String, String>();
+
+ entry.put(ServiceConstants.call.CALL_TYPE, tryToGet(swigmap, ServiceConstants.call.CALL_TYPE));
+ entry.put(ServiceConstants.call.PEER_NUMBER, tryToGet(swigmap, ServiceConstants.call.PEER_NUMBER));
+ entry.put(ServiceConstants.call.DISPLAY_NAME, tryToGet(swigmap, ServiceConstants.call.DISPLAY_NAME));
+ entry.put(ServiceConstants.call.CALL_STATE, tryToGet(swigmap, ServiceConstants.call.CALL_STATE));
+ entry.put(ServiceConstants.call.CONF_ID, tryToGet(swigmap, ServiceConstants.call.CONF_ID));
+ entry.put(ServiceConstants.call.TIMESTAMP_START, tryToGet(swigmap, ServiceConstants.call.TIMESTAMP_START));
+ entry.put(ServiceConstants.call.ACCOUNTID, tryToGet(swigmap, ServiceConstants.call.ACCOUNTID));
+
+ return entry;
+ }
+
+ public static ArrayList<HashMap<String, String>> convertCredentialsToNative(VectMap map) {
+
+ ArrayList<HashMap<String, String>> toReturn = new ArrayList<HashMap<String, String>>();
+
+ for (int i = 0; i < map.size(); ++i) {
+ StringMap entry;
+ HashMap<String, String> nativeEntry = new HashMap<String, String>();
+ entry = map.get(i);
+ nativeEntry.put(AccountDetailBasic.CONFIG_ACCOUNT_PASSWORD, entry.get(AccountDetailBasic.CONFIG_ACCOUNT_PASSWORD));
+ nativeEntry.put(AccountDetailBasic.CONFIG_ACCOUNT_USERNAME, entry.get(AccountDetailBasic.CONFIG_ACCOUNT_USERNAME));
+ nativeEntry.put(AccountDetailBasic.CONFIG_ACCOUNT_REALM, entry.get(AccountDetailBasic.CONFIG_ACCOUNT_REALM));
+ toReturn.add(nativeEntry);
+ }
+ return toReturn;
+ }
+
+ public static ArrayList<String> convertSwigToNative(StringVect vector) {
+ ArrayList<String> toReturn = new ArrayList<String>();
+ for (int i = 0; i < vector.size(); ++i) {
+ toReturn.add(vector.get(i));
+ }
+ return toReturn;
+ }
+}
diff --git a/ring-android/src/cx/ring/utils/bluetooth/BluetoothUtils14.java b/ring-android/src/cx/ring/utils/bluetooth/BluetoothUtils14.java
new file mode 100644
index 0000000..c378347
--- /dev/null
+++ b/ring-android/src/cx/ring/utils/bluetooth/BluetoothUtils14.java
@@ -0,0 +1,167 @@
+/**
+ * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
+ * This file is part of CSipSimple.
+ *
+ * CSipSimple is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * If you own a pjsip commercial license you can also redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public License
+ * as an android library.
+ *
+ * CSipSimple is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with CSipSimple. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package cx.ring.utils.bluetooth;
+
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.util.Log;
+
+import java.util.Set;
+
+public class BluetoothUtils14 extends BluetoothWrapper {
+
+ private static String TAG = BluetoothUtils14.class.getSimpleName();
+ private AudioManager audioManager;
+ private boolean isBluetoothConnected = false;
+
+
+ @Override
+ public boolean isBTHeadsetConnected() {
+ return bluetoothAdapter != null && (bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET) == BluetoothAdapter.STATE_CONNECTED);
+ }
+
+
+
+ private BroadcastReceiver mediaStateReceiver = new BroadcastReceiver() {
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ Log.d(TAG, ">>> BT SCO state changed !!! ");
+ if(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED.equals(action)) {
+ int status = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, AudioManager.SCO_AUDIO_STATE_ERROR );
+ Log.d(TAG, "BT SCO state changed : " + status + " target is " + targetBt);
+ audioManager.setBluetoothScoOn(targetBt);
+
+ if(status == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
+ isBluetoothConnected = true;
+ }else if(status == AudioManager.SCO_AUDIO_STATE_DISCONNECTED) {
+ isBluetoothConnected = false;
+ }
+
+ if(btChangesListener != null) {
+ btChangesListener.onBluetoothStateChanged(status);
+ }
+ }
+ }
+ };
+
+ protected BluetoothAdapter bluetoothAdapter;
+
+ @Override
+ public void setContext(Context aContext){
+ super.setContext(aContext);
+ audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ if(bluetoothAdapter == null) {
+ try {
+ bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ }catch(RuntimeException e) {
+ Log.w(TAG, "Cant get default bluetooth adapter ", e);
+ }
+ }
+ }
+
+ public boolean canBluetooth() {
+ // Detect if any bluetooth a device is available for call
+ if (bluetoothAdapter == null) {
+ // Device does not support Bluetooth
+ return false;
+ }
+ boolean hasConnectedDevice = false;
+ //If bluetooth is on
+ if(bluetoothAdapter.isEnabled()) {
+
+ //We get all bounded bluetooth devices
+ // bounded is not enough, should search for connected devices....
+ Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
+ for(BluetoothDevice device : pairedDevices) {
+ BluetoothClass bluetoothClass = device.getBluetoothClass();
+ if (bluetoothClass != null) {
+ int deviceClass = bluetoothClass.getDeviceClass();
+ if(bluetoothClass.hasService(BluetoothClass.Service.RENDER) ||
+ deviceClass == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET ||
+ deviceClass == BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO ||
+ deviceClass == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ) {
+ //And if any can be used as a audio handset
+ hasConnectedDevice = true;
+ break;
+ }
+ }
+ }
+ }
+ boolean retVal = hasConnectedDevice && audioManager.isBluetoothScoAvailableOffCall();
+ Log.d(TAG, "Can I do BT ? "+retVal);
+ return retVal;
+ }
+
+ private boolean targetBt = false;
+ public void setBluetoothOn(boolean on) {
+ Log.d(TAG, "Ask for "+on+" vs "+audioManager.isBluetoothScoOn());
+ targetBt = on;
+ if(on != isBluetoothConnected) {
+ // BT SCO connection state is different from required activation
+ if(on) {
+ // First we try to connect
+ Log.d(TAG, "BT SCO on >>>");
+ audioManager.startBluetoothSco();
+ }else {
+ Log.d(TAG, "BT SCO off >>>");
+ // We stop to use BT SCO
+ audioManager.setBluetoothScoOn(false);
+ // And we stop BT SCO connection
+ audioManager.stopBluetoothSco();
+ }
+ }else if(on != audioManager.isBluetoothScoOn()) {
+ // BT SCO is already in desired connection state
+ // we only have to use it
+ audioManager.setBluetoothScoOn(on);
+ }
+ }
+
+ public boolean isBluetoothOn() {
+ return isBluetoothConnected;
+ }
+
+ @SuppressWarnings("deprecation")
+ public void register() {
+ Log.d(TAG, "Register BT media receiver");
+ context.registerReceiver(mediaStateReceiver , new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED));
+ }
+
+ public void unregister() {
+ try {
+ Log.d(TAG, "Unregister BT media receiver");
+ context.unregisterReceiver(mediaStateReceiver);
+ }catch(Exception e) {
+ Log.w(TAG, "Failed to unregister media state receiver",e);
+ }
+ }
+}
diff --git a/ring-android/src/cx/ring/utils/bluetooth/BluetoothWrapper.java b/ring-android/src/cx/ring/utils/bluetooth/BluetoothWrapper.java
new file mode 100644
index 0000000..053db85
--- /dev/null
+++ b/ring-android/src/cx/ring/utils/bluetooth/BluetoothWrapper.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
+ * This file is part of CSipSimple.
+ *
+ * CSipSimple is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * If you own a pjsip commercial license you can also redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public License
+ * as an android library.
+ *
+ * CSipSimple is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with CSipSimple. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package cx.ring.utils.bluetooth;
+
+import android.content.Context;
+
+
+public abstract class BluetoothWrapper {
+
+ public interface BluetoothChangeListener {
+ void onBluetoothStateChanged(int status);
+ }
+
+
+ private static BluetoothWrapper instance;
+ protected Context context;
+
+ protected BluetoothChangeListener btChangesListener;
+
+ public static BluetoothWrapper getInstance(Context context) {
+ if (instance == null) {
+ instance = new BluetoothUtils14();
+ instance.setContext(context);
+ }
+
+ return instance;
+ }
+
+ protected BluetoothWrapper() {
+ }
+
+ protected void setContext(Context ctxt) {
+ context = ctxt;
+ }
+
+ public void setBluetoothChangeListener(BluetoothChangeListener l) {
+ btChangesListener = l;
+ }
+
+ public abstract boolean canBluetooth();
+
+ public abstract void setBluetoothOn(boolean on);
+
+ public abstract boolean isBluetoothOn();
+
+ public abstract void register();
+
+ public abstract void unregister();
+
+ public abstract boolean isBTHeadsetConnected();
+}
diff --git a/ring-android/src/cx/ring/views/CallPaneLayout.java b/ring-android/src/cx/ring/views/CallPaneLayout.java
new file mode 100644
index 0000000..c9bd686
--- /dev/null
+++ b/ring-android/src/cx/ring/views/CallPaneLayout.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Adrien Beraud <adrien.beraud@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.views;
+
+import cx.ring.fragments.CallFragment;
+
+import android.content.Context;
+import android.support.v4.widget.SlidingPaneLayout;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+public class CallPaneLayout extends SlidingPaneLayout
+{
+ private CallFragment curFragment = null;
+
+ public CallFragment getCurFragment() {
+ return curFragment;
+ }
+
+ public void setCurFragment(CallFragment curFragment) {
+ this.curFragment = curFragment;
+ }
+
+ public CallPaneLayout(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ }
+
+ public CallPaneLayout(Context context, AttributeSet attrs, int defStyle)
+ {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event)
+ {
+ if(curFragment!=null && !curFragment.canOpenIMPanel()) {
+ return false;
+ }
+
+ return super.onInterceptTouchEvent(event);
+ }
+
+}
diff --git a/ring-android/src/cx/ring/views/CircularImageView.java b/ring-android/src/cx/ring/views/CircularImageView.java
new file mode 100644
index 0000000..ad5e73c
--- /dev/null
+++ b/ring-android/src/cx/ring/views/CircularImageView.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.views;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Shader;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+public class CircularImageView extends ImageView
+{
+ private int borderWidth = 4;
+ private int viewWidth;
+ private int viewHeight;
+ private Bitmap image;
+ private Paint paint;
+ private Paint paintBorder;
+ private BitmapShader shader;
+
+ public CircularImageView(Context context)
+ {
+ super(context);
+ setup();
+ }
+
+ public CircularImageView(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ setup();
+ }
+
+ public CircularImageView(Context context, AttributeSet attrs, int defStyle)
+ {
+ super(context, attrs, defStyle);
+ setup();
+ }
+
+ private void setup()
+ {
+ // init paint
+ paint = new Paint();
+ paint.setAntiAlias(true);
+
+ paintBorder = new Paint();
+ setBorderColor(Color.WHITE);
+ paintBorder.setAntiAlias(true);
+ this.setLayerType(LAYER_TYPE_SOFTWARE, paintBorder);
+ paintBorder.setShadowLayer(4.0f, 0.0f, 2.0f, Color.BLACK);
+ }
+
+ public void setBorderWidth(int borderWidth)
+ {
+ this.borderWidth = borderWidth;
+ this.invalidate();
+ }
+
+ public void setBorderColor(int borderColor)
+ {
+ if (paintBorder != null)
+ paintBorder.setColor(borderColor);
+
+ this.invalidate();
+ }
+
+ private void loadBitmap()
+ {
+ BitmapDrawable bitmapDrawable = (BitmapDrawable) this.getDrawable();
+
+ if (bitmapDrawable != null)
+ image = bitmapDrawable.getBitmap();
+ }
+
+ @SuppressLint("DrawAllocation")
+ @Override
+ public void onDraw(Canvas canvas)
+ {
+ // load the bitmap
+ loadBitmap();
+
+ // init shader
+ if (image != null)
+ {
+ shader = new BitmapShader(Bitmap.createScaledBitmap(image, canvas.getWidth(), canvas.getHeight(), false), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+ paint.setShader(shader);
+ int circleCenter = viewWidth / 2;
+
+ // circleCenter is the x or y of the view's center
+ // radius is the radius in pixels of the cirle to be drawn
+ // paint contains the shader that will texture the shape
+ canvas.drawCircle(circleCenter + borderWidth, circleCenter + borderWidth, circleCenter + borderWidth - 4.0f, paintBorder);
+ canvas.drawCircle(circleCenter + borderWidth, circleCenter + borderWidth, circleCenter - 4.0f, paint);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
+ {
+ int width = measureWidth(widthMeasureSpec);
+ int height = measureHeight(heightMeasureSpec, widthMeasureSpec);
+
+ viewWidth = width - (borderWidth * 2);
+ viewHeight = height - (borderWidth * 2);
+
+ setMeasuredDimension(width, height);
+ }
+
+ private int measureWidth(int measureSpec)
+ {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ if (specMode == MeasureSpec.EXACTLY)
+ {
+ // We were told how big to be
+ result = specSize;
+ }
+ else
+ {
+ // Measure the text
+ result = viewWidth;
+ }
+
+ return result;
+ }
+
+ private int measureHeight(int measureSpecHeight, int measureSpecWidth)
+ {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpecHeight);
+ int specSize = MeasureSpec.getSize(measureSpecHeight);
+
+ if (specMode == MeasureSpec.EXACTLY)
+ {
+ // We were told how big to be
+ result = specSize;
+ }
+ else
+ {
+ // Measure the text (beware: ascent is a negative number)
+ result = viewHeight;
+ }
+
+ return (result + 2);
+ }
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/views/ClearableEditText.java b/ring-android/src/cx/ring/views/ClearableEditText.java
new file mode 100644
index 0000000..9527fd0
--- /dev/null
+++ b/ring-android/src/cx/ring/views/ClearableEditText.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.views;
+
+import cx.ring.R;
+
+import android.content.Context;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.DragEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.RelativeLayout;
+import android.widget.TextView.OnEditorActionListener;
+
+
+
+public class ClearableEditText extends RelativeLayout {
+ LayoutInflater inflater = null;
+ EditText edit_text;
+ Button btn_clear;
+ private TextWatcher watch = null;
+
+ public ClearableEditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initViews();
+ }
+
+ public ClearableEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initViews();
+ }
+
+ public ClearableEditText(Context context) {
+ super(context);
+ // TODO Auto-generated constructor stub
+ initViews();
+ }
+
+ void initViews() {
+ inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.clearable_edit_text, this, true);
+ edit_text = (EditText) findViewById(R.id.clearable_edit);
+ edit_text.setSingleLine();
+ edit_text.setImeOptions(EditorInfo.IME_ACTION_DONE);
+ btn_clear = (Button) findViewById(R.id.clearable_button_clear);
+ btn_clear.setVisibility(RelativeLayout.INVISIBLE);
+
+ // Dummy listener to fix an sdk issue: https://code.google.com/p/android/issues/detail?id=21775
+ edit_text.setOnDragListener(new OnDragListener() {
+
+ @Override
+ public boolean onDrag(View v, DragEvent event) {
+ if (event.getAction() == DragEvent.ACTION_DROP)
+ return true;
+ else
+ return false;
+ }
+ });
+ clearText();
+ showHideClearButton();
+ }
+
+ void clearText() {
+ btn_clear.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ edit_text.setText("");
+ }
+ });
+ }
+
+ void showHideClearButton() {
+ edit_text.addTextChangedListener(new TextWatcher() {
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (s.length() > 0)
+ btn_clear.setVisibility(RelativeLayout.VISIBLE);
+ else
+ btn_clear.setVisibility(RelativeLayout.INVISIBLE);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+ }
+
+ public Editable getText() {
+ Editable text = edit_text.getText();
+ return text;
+ }
+
+ public void setInputType(int typeClassNumber) {
+ edit_text.setFocusableInTouchMode(true);
+ edit_text.requestFocus();
+ edit_text.setInputType(typeClassNumber);
+
+ }
+
+ public EditText getEdit_text() {
+ return edit_text;
+ }
+
+ public void setError(String string) {
+ edit_text.setError(string);
+ edit_text.requestFocus();
+ }
+
+ public void setTextWatcher(TextWatcher l) {
+ watch = l;
+ edit_text.addTextChangedListener(watch);
+ }
+
+ public void unsetTextWatcher() {
+ edit_text.removeTextChangedListener(watch);
+ }
+
+ public void setOnEditorActionListener(OnEditorActionListener onEditorActionListener) {
+ edit_text.setOnEditorActionListener(onEditorActionListener);
+
+ }
+}
diff --git a/ring-android/src/cx/ring/views/CredentialsPreference.java b/ring-android/src/cx/ring/views/CredentialsPreference.java
new file mode 100644
index 0000000..24f3d30
--- /dev/null
+++ b/ring-android/src/cx/ring/views/CredentialsPreference.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.views;
+
+import java.util.HashMap;
+
+import cx.ring.R;
+
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
+import cx.ring.model.account.AccountCredentials;
+import cx.ring.model.account.CredentialsManager;
+
+public class CredentialsPreference extends DialogPreference {
+
+ EditText mUsernameField;
+ PasswordEditText mPasswordField;
+ EditText mRealmField;
+
+ public CredentialsPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.credentials_pref, null);
+
+ mUsernameField = (EditText) view.findViewById(R.id.credentials_username);
+ mPasswordField = (PasswordEditText) view.findViewById(R.id.credentials_password);
+ mRealmField = (EditText) view.findViewById(R.id.credentials_realm);
+
+ if (getExtras().getSerializable(CredentialsManager.CURRENT_CRED) != null) {
+ HashMap<String, String> details = (HashMap<String, String>) getExtras().getSerializable(CredentialsManager.CURRENT_CRED);
+ mUsernameField.setText(details.get(AccountCredentials.CONFIG_ACCOUNT_USERNAME));
+ mPasswordField.getEdit_text().setText(details.get(AccountCredentials.CONFIG_ACCOUNT_PASSWORD));
+ mRealmField.setText(details.get(AccountCredentials.CONFIG_ACCOUNT_REALM));
+ }
+
+ mRealmField.setOnEditorActionListener(new OnEditorActionListener() {
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ String to = mRealmField.getText().toString();
+ if (to.contentEquals("")) {
+ mRealmField.setError(getContext().getString(R.string.dial_error_no_number_dialed));
+ }
+ return true;
+ }
+ });
+
+ return view;
+ }
+
+ private boolean isValid() {
+ return mUsernameField.getText().length() > 0 && mPasswordField.getText().length() > 0 && mRealmField.getText().length() > 0;
+ }
+
+ @Override
+ protected void showDialog(Bundle state) {
+ super.showDialog(state);
+
+ final AlertDialog d = (AlertDialog) getDialog();
+
+ // Prevent dismissing the dialog if they are any empty field
+ d.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ if (isValid()) {
+ d.dismiss();
+ onDialogClosed(true);
+ } else {
+ Toast t = Toast.makeText(getContext(), "All fields are mandatory!", Toast.LENGTH_LONG);
+ t.setGravity(Gravity.CENTER, 0, 0);
+ t.show();
+ }
+ }
+ });
+
+ d.setButton(DialogInterface.BUTTON_NEUTRAL, "Delete", new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Bundle toReturn = getExtras();
+ getOnPreferenceChangeListener().onPreferenceChange(CredentialsPreference.this, toReturn);
+ }
+ });
+
+ }
+
+ @Override
+ public void onPrepareDialogBuilder(Builder builder) {
+
+ if (getExtras().getSerializable(CredentialsManager.CURRENT_CRED) != null) {
+ // If the user is editing an entry, he can delete it, otherwise don't show this button
+ builder.setNeutralButton("Delete", new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Bundle toReturn = getExtras();
+ getOnPreferenceChangeListener().onPreferenceChange(CredentialsPreference.this, toReturn);
+ }
+ });
+ }
+ super.onPrepareDialogBuilder(builder);
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ if (positiveResult) {
+ if (getExtras().getSerializable(CredentialsManager.CURRENT_CRED) != null) {
+ Bundle toReturn = getExtras();
+ HashMap<String, String> fields = new HashMap<String, String>();
+ fields.put(AccountCredentials.CONFIG_ACCOUNT_USERNAME, mUsernameField.getText().toString());
+ fields.put(AccountCredentials.CONFIG_ACCOUNT_PASSWORD, mPasswordField.getText().toString());
+ fields.put(AccountCredentials.CONFIG_ACCOUNT_REALM, mRealmField.getText().toString());
+ toReturn.putSerializable(CredentialsManager.NEW_CRED, fields);
+ getOnPreferenceChangeListener().onPreferenceChange(this, toReturn);
+ } else {
+ HashMap<String, String> fields = new HashMap<String, String>();
+ fields.put(AccountCredentials.CONFIG_ACCOUNT_USERNAME, mUsernameField.getText().toString());
+ fields.put(AccountCredentials.CONFIG_ACCOUNT_PASSWORD, mPasswordField.getText().toString());
+ fields.put(AccountCredentials.CONFIG_ACCOUNT_REALM, mRealmField.getText().toString());
+ getOnPreferenceChangeListener().onPreferenceChange(this, new AccountCredentials(fields));
+ }
+
+ }
+ }
+
+}
diff --git a/ring-android/src/cx/ring/views/HalfCircleImageView.java b/ring-android/src/cx/ring/views/HalfCircleImageView.java
new file mode 100644
index 0000000..e9b0d6a
--- /dev/null
+++ b/ring-android/src/cx/ring/views/HalfCircleImageView.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.views;
+
+import cx.ring.R;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+public class HalfCircleImageView extends ImageView
+{
+ private int borderWidth = 0;
+ private int viewWidth;
+ private int viewHeight;
+ private Bitmap image;
+ private Paint paint;
+ private Paint paintBorder;
+ private Paint backgroundPaint;
+ private RectF viewBounds;
+
+
+ public HalfCircleImageView(Context context)
+ {
+ super(context);
+ setup();
+ }
+
+ public HalfCircleImageView(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ setup();
+ }
+
+ public HalfCircleImageView(Context context, AttributeSet attrs, int defStyle)
+ {
+ super(context, attrs, defStyle);
+ setup();
+ }
+
+ private void setup()
+ {
+ backgroundPaint = new Paint();
+ backgroundPaint.setColor(getResources().getColor(R.color.sfl_dark_blue));
+ backgroundPaint.setAntiAlias(true);
+ // init paint
+ paint = new Paint();
+ paint.setAntiAlias(true);
+
+ viewBounds = new RectF();
+
+ paintBorder = new Paint();
+ setBorderColor(Color.WHITE);
+ paintBorder.setAntiAlias(true);
+ this.setLayerType(LAYER_TYPE_SOFTWARE, paintBorder);
+ paintBorder.setShadowLayer(4.0f, 0.0f, 2.0f, Color.BLACK);
+ }
+
+ public void setBorderWidth(int borderWidth)
+ {
+ this.borderWidth = borderWidth;
+ this.invalidate();
+ }
+
+ public void setBorderColor(int borderColor)
+ {
+ if (paintBorder != null)
+ paintBorder.setColor(borderColor);
+
+ this.invalidate();
+ }
+
+ private void loadBitmap()
+ {
+ BitmapDrawable bitmapDrawable = (BitmapDrawable) this.getDrawable();
+
+ if (bitmapDrawable != null)
+ image = bitmapDrawable.getBitmap();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas)
+ {
+ // load the bitmap
+ loadBitmap();
+
+ canvas.drawArc(viewBounds, 180, 180, false, backgroundPaint);
+
+ // init shader
+ if (image != null)
+ {
+// shader = new BitmapShader(Bitmap.createScaledBitmap(image, canvas.getWidth(), canvas.getHeight(), false), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+// paint.setShader(shader);
+// int circleCenter = viewWidth / 2;
+
+ // circleCenter is the x or y of the view's center
+ // radius is the radius in pixels of the cirle to be drawn
+ // paint contains the shader that will texture the shape
+// canvas.drawCircle(circleCenter + borderWidth, circleCenter + borderWidth, circleCenter + borderWidth - 4.0f, paintBorder);
+ canvas.drawBitmap(image, viewWidth / 2 - image.getWidth() / 2, viewHeight / 3 - image.getHeight() / 2, paint);
+
+
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
+ {
+ int width = measureWidth(widthMeasureSpec);
+ int height = measureHeight(heightMeasureSpec, widthMeasureSpec);
+
+ viewWidth = width - (borderWidth * 2);
+ viewHeight = height - (borderWidth * 2);
+
+ viewBounds.set(0, 0, width, height);
+
+ setMeasuredDimension(width, height);
+ }
+
+ private int measureWidth(int measureSpec)
+ {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ if (specMode == MeasureSpec.EXACTLY)
+ {
+ // We were told how big to be
+ result = specSize;
+ }
+ else
+ {
+ // Measure the text
+ result = viewWidth;
+ }
+
+ return result;
+ }
+
+ private int measureHeight(int measureSpecHeight, int measureSpecWidth)
+ {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpecHeight);
+ int specSize = MeasureSpec.getSize(measureSpecHeight);
+
+ if (specMode == MeasureSpec.EXACTLY)
+ {
+ // We were told how big to be
+ result = specSize;
+ }
+ else
+ {
+ // Measure the text (beware: ascent is a negative number)
+ result = viewHeight;
+ }
+
+ return (result);
+ }
+
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/views/NumberPickerPreference.java b/ring-android/src/cx/ring/views/NumberPickerPreference.java
new file mode 100644
index 0000000..bbf7342
--- /dev/null
+++ b/ring-android/src/cx/ring/views/NumberPickerPreference.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.views;
+
+import java.lang.reflect.Field;
+
+import cx.ring.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.NumberPicker;
+
+public class NumberPickerPreference extends DialogPreference {
+ private int mMin, mMax, mDefault;
+
+ private String mMaxExternalKey, mMinExternalKey;
+
+ private NumberPicker mNumberPicker;
+
+ public NumberPickerPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ // TypedArray dialogType = context.obtainStyledAttributes(attrs,
+ // com.android.internal.R.styleable.DialogPreference, 0, 0);
+ TypedArray numberPickerType = context.obtainStyledAttributes(attrs, R.styleable.NumberPickerPreference, 0, 0);
+
+ mMaxExternalKey = numberPickerType.getString(R.styleable.NumberPickerPreference_maxExternal);
+ mMinExternalKey = numberPickerType.getString(R.styleable.NumberPickerPreference_minExternal);
+
+ mMax = numberPickerType.getInt(R.styleable.NumberPickerPreference_max, 5);
+ mMin = numberPickerType.getInt(R.styleable.NumberPickerPreference_min, 0);
+
+ // mDefault = dialogType.getInt(com.android.internal.R.styleable.Preference_defaultValue, mMin);
+ mDefault = mMin;
+ // dialogType.recycle();
+ numberPickerType.recycle();
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+ int max = mMax;
+ int min = mMin;
+
+ // External values
+ if (mMaxExternalKey != null) {
+ max = getSharedPreferences().getInt(mMaxExternalKey, mMax);
+ }
+ if (mMinExternalKey != null) {
+ min = getSharedPreferences().getInt(mMinExternalKey, mMin);
+ }
+
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.number_picker_dialog, null);
+
+ mNumberPicker = (NumberPicker) view.findViewById(R.id.number_picker);
+
+ if (mNumberPicker == null) {
+ throw new RuntimeException("mNumberPicker is null!");
+ }
+
+ // Initialize state
+ mNumberPicker.setWrapSelectorWheel(false);
+ mNumberPicker.setMaxValue(max);
+ mNumberPicker.setMinValue(min);
+ mNumberPicker.setValue(getPersistedInt(mDefault));
+
+ // No keyboard popup
+ disableTextInput(mNumberPicker);
+ // EditText textInput = (EditText) mNumberPicker.findViewById(com.android.internal.R.id.numberpicker_input);
+ // if (textInput != null) {
+ // textInput.setCursorVisible(false);
+ // textInput.setFocusable(false);
+ // textInput.setFocusableInTouchMode(false);
+ // }
+
+ return view;
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ if (positiveResult) {
+ persistInt(mNumberPicker.getValue());
+ getOnPreferenceChangeListener().onPreferenceChange(this, String.valueOf(mNumberPicker.getValue()));
+ }
+ }
+
+ /*
+ * reflection of NumberPicker.java verified in 4.1, 4.2
+ */
+ private void disableTextInput(NumberPicker np) {
+ if (np == null)
+ return;
+ Class<?> classType = np.getClass();
+ Field inputTextField;
+ try {
+ inputTextField = classType.getDeclaredField("mInputText");
+ inputTextField.setAccessible(true);
+ EditText textInput = (EditText) inputTextField.get(np);
+ if (textInput != null) {
+ textInput.setCursorVisible(false);
+ textInput.setFocusable(false);
+ textInput.setFocusableInTouchMode(false);
+ }
+ } catch (Exception e) {
+ Log.d("trebuchet", "NumberPickerPreference disableTextInput error", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/views/PagerSlidingTabStrip.java b/ring-android/src/cx/ring/views/PagerSlidingTabStrip.java
new file mode 100644
index 0000000..ff363c5
--- /dev/null
+++ b/ring-android/src/cx/ring/views/PagerSlidingTabStrip.java
@@ -0,0 +1,616 @@
+/*
+ * Copyright (C) 2013 Andreas Stuetz <andreas.stuetz@gmail.com>
+ *
+ * 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 cx.ring.views;
+
+import java.util.Locale;
+
+import cx.ring.R;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.ViewPager.OnPageChangeListener;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.HorizontalScrollView;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class PagerSlidingTabStrip extends HorizontalScrollView {
+
+ public interface IconTabProvider {
+ public int getPageIconResId(int position);
+ }
+
+ // @formatter:off
+ private static final int[] ATTRS = new int[] { android.R.attr.textSize, android.R.attr.textColor };
+ // @formatter:on
+
+ private LinearLayout.LayoutParams defaultTabLayoutParams;
+ private LinearLayout.LayoutParams expandedTabLayoutParams;
+
+ private final PageListener pageListener = new PageListener();
+ public OnPageChangeListener delegatePageListener;
+
+ private LinearLayout tabsContainer;
+ private ViewPager pager;
+
+ private int tabCount;
+
+ private int currentPosition = 0;
+ private float currentPositionOffset = 0f;
+
+ private Paint rectPaint;
+ private Paint dividerPaint;
+
+ private boolean checkedTabWidths = false;
+
+ private int indicatorColor = 0xFF666666;
+ private int underlineColor = 0x1A000000;
+ private int dividerColor = 0x1A000000;
+
+ private boolean shouldExpand = false;
+ private boolean textAllCaps = true;
+
+ private int scrollOffset = 52;
+ private int indicatorHeight = 8;
+ private int underlineHeight = 2;
+ private int dividerPadding = 12;
+ private int tabPadding = 24;
+ private int dividerWidth = 1;
+
+ private int tabTextSize = 12;
+ private int tabTextColor = 0xFF666666;
+ private Typeface tabTypeface = null;
+ private int tabTypefaceStyle = Typeface.BOLD;
+
+ private int lastScrollX = 0;
+
+ private int tabBackgroundResId = R.drawable.background_tabs;
+
+ private Locale locale;
+
+ public PagerSlidingTabStrip(Context context) {
+ this(context, null);
+ }
+
+ public PagerSlidingTabStrip(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PagerSlidingTabStrip(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ setFillViewport(true);
+ setWillNotDraw(false);
+
+ tabsContainer = new LinearLayout(context);
+ tabsContainer.setOrientation(LinearLayout.HORIZONTAL);
+ tabsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ addView(tabsContainer);
+
+ DisplayMetrics dm = getResources().getDisplayMetrics();
+
+ scrollOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, scrollOffset, dm);
+ indicatorHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, indicatorHeight, dm);
+ underlineHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, underlineHeight, dm);
+ dividerPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dividerPadding, dm);
+ tabPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, tabPadding, dm);
+ dividerWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dividerWidth, dm);
+ tabTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, tabTextSize, dm);
+
+ // get system attrs (android:textSize and android:textColor)
+
+ TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
+
+ tabTextSize = a.getDimensionPixelSize(0, tabTextSize);
+ tabTextColor = a.getColor(1, tabTextColor);
+
+ a.recycle();
+
+ // get custom attrs
+
+ a = context.obtainStyledAttributes(attrs, R.styleable.PagerSlidingTabStrip);
+
+ indicatorColor = a.getColor(R.styleable.PagerSlidingTabStrip_indicatorColor, indicatorColor);
+ underlineColor = a.getColor(R.styleable.PagerSlidingTabStrip_underlineColor, underlineColor);
+ dividerColor = a.getColor(R.styleable.PagerSlidingTabStrip_dividerColor, dividerColor);
+ indicatorHeight = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_indicatorHeight, indicatorHeight);
+ underlineHeight = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_underlineHeight, underlineHeight);
+ dividerPadding = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_dividerPadding, dividerPadding);
+ tabPadding = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_tabPaddingLeftRight, tabPadding);
+ tabBackgroundResId = a.getResourceId(R.styleable.PagerSlidingTabStrip_tabBackground, tabBackgroundResId);
+ shouldExpand = a.getBoolean(R.styleable.PagerSlidingTabStrip_shouldExpand, shouldExpand);
+ scrollOffset = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_scrollOffset, scrollOffset);
+ textAllCaps = a.getBoolean(R.styleable.PagerSlidingTabStrip_textAllCaps, textAllCaps);
+
+ a.recycle();
+
+ rectPaint = new Paint();
+ rectPaint.setAntiAlias(true);
+ rectPaint.setStyle(Style.FILL);
+
+ dividerPaint = new Paint();
+ dividerPaint.setAntiAlias(true);
+ dividerPaint.setStrokeWidth(dividerWidth);
+
+ defaultTabLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+ expandedTabLayoutParams = new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f);
+
+ if (locale == null) {
+ locale = getResources().getConfiguration().locale;
+ }
+ }
+
+ public void setViewPager(ViewPager pager) {
+ this.pager = pager;
+
+ if (pager.getAdapter() == null) {
+ throw new IllegalStateException("ViewPager does not have adapter instance.");
+ }
+
+ pager.setOnPageChangeListener(pageListener);
+
+ notifyDataSetChanged();
+ }
+
+ public void setOnPageChangeListener(OnPageChangeListener listener) {
+ this.delegatePageListener = listener;
+ }
+
+ public void notifyDataSetChanged() {
+
+ tabsContainer.removeAllViews();
+
+ tabCount = pager.getAdapter().getCount();
+
+ for (int i = 0; i < tabCount; i++) {
+
+ if (pager.getAdapter() instanceof IconTabProvider) {
+ addIconTab(i, ((IconTabProvider) pager.getAdapter()).getPageIconResId(i));
+ } else {
+ addTextTab(i, pager.getAdapter().getPageTitle(i).toString());
+ }
+
+ }
+
+ updateTabStyles();
+
+ checkedTabWidths = false;
+
+ getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
+
+ @SuppressWarnings("deprecation")
+ @SuppressLint("NewApi")
+ @Override
+ public void onGlobalLayout() {
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ } else {
+ getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+
+ currentPosition = pager.getCurrentItem();
+ scrollToChild(currentPosition, 0);
+ }
+ });
+
+ }
+
+ private void addTextTab(final int position, String title) {
+
+ TextView tab = new TextView(getContext());
+ tab.setText(title);
+ tab.setFocusable(true);
+ tab.setGravity(Gravity.CENTER);
+ tab.setSingleLine();
+
+ tab.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ pager.setCurrentItem(position);
+ }
+ });
+
+ tabsContainer.addView(tab);
+
+ }
+
+ private void addIconTab(final int position, int resId) {
+
+ ImageButton tab = new ImageButton(getContext());
+ tab.setFocusable(true);
+ tab.setImageResource(resId);
+
+ tab.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ pager.setCurrentItem(position);
+ }
+ });
+
+ tabsContainer.addView(tab);
+
+ }
+
+ private void updateTabStyles() {
+
+ for (int i = 0; i < tabCount; i++) {
+
+ View v = tabsContainer.getChildAt(i);
+
+ v.setLayoutParams(defaultTabLayoutParams);
+ v.setBackgroundResource(tabBackgroundResId);
+ if (shouldExpand) {
+ v.setPadding(0, 0, 0, 0);
+ } else {
+ v.setPadding(tabPadding, 0, tabPadding, 0);
+ }
+
+ if (v instanceof TextView) {
+
+ TextView tab = (TextView) v;
+ tab.setTextSize(TypedValue.COMPLEX_UNIT_PX, tabTextSize);
+ tab.setTypeface(tabTypeface, tabTypefaceStyle);
+ tab.setTextColor(tabTextColor);
+
+ // setAllCaps() is only available from API 14, so the upper case is made manually if we are on a
+ // pre-ICS-build
+ if (textAllCaps) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ tab.setAllCaps(true);
+ } else {
+ tab.setText(tab.getText().toString().toUpperCase(locale));
+ }
+ }
+ }
+ }
+
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (!shouldExpand || MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) {
+ return;
+ }
+
+ int myWidth = getMeasuredWidth();
+ int childWidth = 0;
+ for (int i = 0; i < tabCount; i++) {
+ childWidth += tabsContainer.getChildAt(i).getMeasuredWidth();
+ }
+
+ if (!checkedTabWidths && childWidth > 0 && myWidth > 0) {
+
+ if (childWidth <= myWidth) {
+ for (int i = 0; i < tabCount; i++) {
+ tabsContainer.getChildAt(i).setLayoutParams(expandedTabLayoutParams);
+ }
+ }
+
+ checkedTabWidths = true;
+ }
+ }
+
+ private void scrollToChild(int position, int offset) {
+
+ if (tabCount == 0) {
+ return;
+ }
+
+ int newScrollX = tabsContainer.getChildAt(position).getLeft() + offset;
+
+ if (position > 0 || offset > 0) {
+ newScrollX -= scrollOffset;
+ }
+
+ if (newScrollX != lastScrollX) {
+ lastScrollX = newScrollX;
+ scrollTo(newScrollX, 0);
+ }
+
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (isInEditMode() || tabCount == 0) {
+ return;
+ }
+
+ final int height = getHeight();
+
+ // draw indicator line
+
+ rectPaint.setColor(indicatorColor);
+
+ // default: line below current tab
+ View currentTab = tabsContainer.getChildAt(currentPosition);
+ float lineLeft = currentTab.getLeft();
+ float lineRight = currentTab.getRight();
+
+ // if there is an offset, start interpolating left and right coordinates between current and next tab
+ if (currentPositionOffset > 0f && currentPosition < tabCount - 1) {
+
+ View nextTab = tabsContainer.getChildAt(currentPosition + 1);
+ final float nextTabLeft = nextTab.getLeft();
+ final float nextTabRight = nextTab.getRight();
+
+ lineLeft = (currentPositionOffset * nextTabLeft + (1f - currentPositionOffset) * lineLeft);
+ lineRight = (currentPositionOffset * nextTabRight + (1f - currentPositionOffset) * lineRight);
+ }
+
+ canvas.drawRect(lineLeft, height - indicatorHeight, lineRight, height, rectPaint);
+
+ // draw underline
+
+ rectPaint.setColor(underlineColor);
+ canvas.drawRect(0, height - underlineHeight, tabsContainer.getWidth(), height, rectPaint);
+
+ // draw divider
+
+ dividerPaint.setColor(dividerColor);
+ for (int i = 0; i < tabCount - 1; i++) {
+ View tab = tabsContainer.getChildAt(i);
+ canvas.drawLine(tab.getRight(), dividerPadding, tab.getRight(), height - dividerPadding, dividerPaint);
+ }
+ }
+
+ private class PageListener implements OnPageChangeListener {
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+
+ currentPosition = position;
+ currentPositionOffset = positionOffset;
+
+ scrollToChild(position, (int) (positionOffset * tabsContainer.getChildAt(position).getWidth()));
+
+ invalidate();
+
+ if (delegatePageListener != null) {
+ delegatePageListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ if (state == ViewPager.SCROLL_STATE_IDLE) {
+ scrollToChild(pager.getCurrentItem(), 0);
+ }
+
+ if (delegatePageListener != null) {
+ delegatePageListener.onPageScrollStateChanged(state);
+ }
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ if (delegatePageListener != null) {
+ delegatePageListener.onPageSelected(position);
+ }
+ }
+
+ }
+
+ public void setIndicatorColor(int indicatorColor) {
+ this.indicatorColor = indicatorColor;
+ invalidate();
+ }
+
+ public void setIndicatorColorResource(int resId) {
+ this.indicatorColor = getResources().getColor(resId);
+ invalidate();
+ }
+
+ public int getIndicatorColor() {
+ return this.indicatorColor;
+ }
+
+ public void setIndicatorHeight(int indicatorLineHeightPx) {
+ this.indicatorHeight = indicatorLineHeightPx;
+ invalidate();
+ }
+
+ public int getIndicatorHeight() {
+ return indicatorHeight;
+ }
+
+ public void setUnderlineColor(int underlineColor) {
+ this.underlineColor = underlineColor;
+ invalidate();
+ }
+
+ public void setUnderlineColorResource(int resId) {
+ this.underlineColor = getResources().getColor(resId);
+ invalidate();
+ }
+
+ public int getUnderlineColor() {
+ return underlineColor;
+ }
+
+ public void setDividerColor(int dividerColor) {
+ this.dividerColor = dividerColor;
+ invalidate();
+ }
+
+ public void setDividerColorResource(int resId) {
+ this.dividerColor = getResources().getColor(resId);
+ invalidate();
+ }
+
+ public int getDividerColor() {
+ return dividerColor;
+ }
+
+ public void setUnderlineHeight(int underlineHeightPx) {
+ this.underlineHeight = underlineHeightPx;
+ invalidate();
+ }
+
+ public int getUnderlineHeight() {
+ return underlineHeight;
+ }
+
+ public void setDividerPadding(int dividerPaddingPx) {
+ this.dividerPadding = dividerPaddingPx;
+ invalidate();
+ }
+
+ public int getDividerPadding() {
+ return dividerPadding;
+ }
+
+ public void setScrollOffset(int scrollOffsetPx) {
+ this.scrollOffset = scrollOffsetPx;
+ invalidate();
+ }
+
+ public int getScrollOffset() {
+ return scrollOffset;
+ }
+
+ public void setShouldExpand(boolean shouldExpand) {
+ this.shouldExpand = shouldExpand;
+ requestLayout();
+ }
+
+ public boolean getShouldExpand() {
+ return shouldExpand;
+ }
+
+ public boolean isTextAllCaps() {
+ return textAllCaps;
+ }
+
+ public void setAllCaps(boolean textAllCaps) {
+ this.textAllCaps = textAllCaps;
+ }
+
+ public void setTextSize(int textSizePx) {
+ this.tabTextSize = textSizePx;
+ updateTabStyles();
+ }
+
+ public int getTextSize() {
+ return tabTextSize;
+ }
+
+ public void setTextColor(int textColor) {
+ this.tabTextColor = textColor;
+ updateTabStyles();
+ }
+
+ public void setTextColorResource(int resId) {
+ this.tabTextColor = getResources().getColor(resId);
+ updateTabStyles();
+ }
+
+ public int getTextColor() {
+ return tabTextColor;
+ }
+
+ public void setTypeface(Typeface typeface, int style) {
+ this.tabTypeface = typeface;
+ this.tabTypefaceStyle = style;
+ updateTabStyles();
+ }
+
+ public void setTabBackground(int resId) {
+ this.tabBackgroundResId = resId;
+ }
+
+ public int getTabBackground() {
+ return tabBackgroundResId;
+ }
+
+ public void setTabPaddingLeftRight(int paddingPx) {
+ this.tabPadding = paddingPx;
+ updateTabStyles();
+ }
+
+ public int getTabPaddingLeftRight() {
+ return tabPadding;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+ currentPosition = savedState.currentPosition;
+ requestLayout();
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState savedState = new SavedState(superState);
+ savedState.currentPosition = currentPosition;
+ return savedState;
+ }
+
+ static class SavedState extends BaseSavedState {
+ int currentPosition;
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ currentPosition = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(currentPosition);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+}
diff --git a/ring-android/src/cx/ring/views/PasswordEditText.java b/ring-android/src/cx/ring/views/PasswordEditText.java
new file mode 100644
index 0000000..a6c62f5
--- /dev/null
+++ b/ring-android/src/cx/ring/views/PasswordEditText.java
@@ -0,0 +1,115 @@
+package cx.ring.views;
+
+import android.content.Context;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.text.method.HideReturnsTransformationMethod;
+import android.text.method.PasswordTransformationMethod;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.RelativeLayout;
+import cx.ring.R;
+
+/**
+ * Created by lisional on 06/04/14.
+ */
+public class PasswordEditText extends RelativeLayout {
+ LayoutInflater inflater = null;
+ EditText edit_text;
+ Button btn_clear;
+
+ public PasswordEditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initViews();
+ }
+
+ public PasswordEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initViews();
+ }
+
+ public PasswordEditText(Context context) {
+ super(context);
+ // TODO Auto-generated constructor stub
+ initViews();
+ }
+
+ void initViews() {
+ inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.password_edittext, this, true);
+ edit_text = (EditText) findViewById(R.id.password_edittext);
+ edit_text.setSingleLine();
+ edit_text.setImeOptions(EditorInfo.IME_ACTION_DONE);
+ btn_clear = (Button) findViewById(R.id.password_visibility);
+ btn_clear.setVisibility(RelativeLayout.INVISIBLE);
+ revealText();
+ edit_text.setTransformationMethod(PasswordTransformationMethod.getInstance());
+ showHideClearButton();
+ }
+
+ void revealText() {
+ btn_clear.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ switch(event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ edit_text.setTransformationMethod(HideReturnsTransformationMethod.getInstance());
+ edit_text.setSelection(edit_text.getText().length());
+ return true; // if you want to handle the touch event
+ case MotionEvent.ACTION_UP:
+ // RELEASED
+ edit_text.setTransformationMethod(PasswordTransformationMethod.getInstance());
+ edit_text.setSelection(edit_text.getText().length());
+ return true; // if you want to handle the touch event
+ }
+ return false;
+ }
+ });
+ }
+
+ void showHideClearButton() {
+ edit_text.addTextChangedListener(new TextWatcher() {
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (s.length() > 0)
+ btn_clear.setVisibility(RelativeLayout.VISIBLE);
+ else
+ btn_clear.setVisibility(RelativeLayout.INVISIBLE);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+ }
+
+ public Editable getText() {
+ Editable text = edit_text.getText();
+ return text;
+ }
+
+ public void setInputType(int typeClassNumber) {
+ edit_text.setFocusableInTouchMode(true);
+ edit_text.requestFocus();
+ edit_text.setInputType(typeClassNumber);
+ }
+
+ public EditText getEdit_text() {
+ return edit_text;
+ }
+
+ public void setError(String string) {
+ edit_text.setError(string);
+ edit_text.requestFocus();
+ }
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/views/PasswordPreference.java b/ring-android/src/cx/ring/views/PasswordPreference.java
new file mode 100644
index 0000000..1727233
--- /dev/null
+++ b/ring-android/src/cx/ring/views/PasswordPreference.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.views;
+
+import cx.ring.R;
+import cx.ring.utils.Compatibility;
+
+import android.content.Context;
+import android.view.View.OnClickListener;
+import android.preference.EditTextPreference;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.CheckBox;
+import android.widget.EditText;
+
+public class PasswordPreference extends EditTextPreference implements OnClickListener, TextWatcher {
+
+ private static final String THIS_FILE = "PasswordPreference";
+ private CheckBox showPwdCheckbox;
+
+ private boolean canShowPassword = false;
+
+ public PasswordPreference(Context context) {
+ this(context, null);
+ }
+
+ public PasswordPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onAddEditTextToDialogView(View dialogView, EditText editText) {
+ super.onAddEditTextToDialogView(dialogView, editText);
+ editText.addTextChangedListener(this);
+ }
+
+ @Override
+ protected void onBindDialogView(View view) {
+ super.onBindDialogView(view);
+ try {
+ if (showPwdCheckbox == null) {
+ showPwdCheckbox = new CheckBox(getContext());
+ showPwdCheckbox.setText(R.string.show_password);
+ showPwdCheckbox.setOnClickListener(this);
+ }
+
+ canShowPassword = TextUtils.isEmpty(getText());
+ getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ updateCanShowPassword();
+ ViewParent oldParent = showPwdCheckbox.getParent();
+ if (oldParent != view) {
+ if (oldParent != null) {
+ ((ViewGroup) oldParent).removeView(showPwdCheckbox);
+ }
+ }
+
+ ViewGroup container = (ViewGroup) view;
+ if (Compatibility.isCompatible(8)) {
+ container = (ViewGroup) container.getChildAt(0);
+ }
+ if (container != null) {
+ container.addView(showPwdCheckbox, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+ } catch (Exception e) {
+ // Just do nothing in case weird ROM in use
+ Log.w(THIS_FILE, "Unsupported device for enhanced password", e);
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (!canShowPassword) {
+ // Even if not shown, be very very sure we never come here
+ return;
+ }
+ getEditText().setInputType(
+ InputType.TYPE_CLASS_TEXT
+ | (((CheckBox) view).isChecked() ? InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD : InputType.TYPE_TEXT_VARIATION_PASSWORD));
+ }
+
+ @Override
+ public void setText(String text) {
+ super.setText(text);
+ setCanShowPassword(TextUtils.isEmpty(text));
+ }
+
+ private void updateCanShowPassword() {
+ if (showPwdCheckbox != null) {
+ showPwdCheckbox.setVisibility(canShowPassword ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ private void setCanShowPassword(boolean canShow) {
+ canShowPassword = canShow;
+ updateCanShowPassword();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (s.length() == 0) {
+ setCanShowPassword(true);
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Nothing to do
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Nothing to do
+ }
+
+}
\ No newline at end of file
diff --git a/ring-android/src/cx/ring/views/QuadNumberPickerPreference.java b/ring-android/src/cx/ring/views/QuadNumberPickerPreference.java
new file mode 100644
index 0000000..49bf9d3
--- /dev/null
+++ b/ring-android/src/cx/ring/views/QuadNumberPickerPreference.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.views;
+
+/*
+ * Copyright (C) 2011 The CyanogenMod 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.
+ */
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.NumberPicker;
+import android.widget.TextView;
+import cx.ring.R;
+
+import java.lang.reflect.Field;
+
+/*
+ * @author Danesh
+ * @author nebkat
+ */
+
+public class QuadNumberPickerPreference extends DialogPreference {
+ private int mMin1, mMax1, mDefault1;
+ private int mMin2, mMax2, mDefault2;
+ private int mMin3, mMax3, mDefault3;
+ private int mMin4, mMax4, mDefault4;
+
+ private String mPickerTitle1;
+ private String mPickerTitle2;
+ private String mPickerTitle3;
+ private String mPickerTitle4;
+
+ private NumberPicker mNumberPicker1;
+ private NumberPicker mNumberPicker2;
+ private NumberPicker mNumberPicker3;
+ private NumberPicker mNumberPicker4;
+
+ public QuadNumberPickerPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ // TypedArray dialogType = context.obtainStyledAttributes(attrs,
+ // com.android.internal.R.styleable.DialogPreference, 0, 0);
+ TypedArray doubleNumberPickerType = context.obtainStyledAttributes(attrs, R.styleable.QuadNumberPickerPreference, 0, 0);
+
+ mPickerTitle1 = doubleNumberPickerType.getString(R.styleable.QuadNumberPickerPreference_pickerTitle1);
+ mPickerTitle2 = doubleNumberPickerType.getString(R.styleable.QuadNumberPickerPreference_pickerTitle2);
+ mPickerTitle3 = doubleNumberPickerType.getString(R.styleable.QuadNumberPickerPreference_pickerTitle3);
+ mPickerTitle4 = doubleNumberPickerType.getString(R.styleable.QuadNumberPickerPreference_pickerTitle4);
+
+
+ mMax1 = doubleNumberPickerType.getInt(R.styleable.QuadNumberPickerPreference_max1, 5);
+ mMin1 = doubleNumberPickerType.getInt(R.styleable.QuadNumberPickerPreference_min1, 0);
+ mMax2 = doubleNumberPickerType.getInt(R.styleable.QuadNumberPickerPreference_max2, 5);
+ mMin2 = doubleNumberPickerType.getInt(R.styleable.QuadNumberPickerPreference_min2, 0);
+ mMax3 = doubleNumberPickerType.getInt(R.styleable.QuadNumberPickerPreference_max3, 5);
+ mMin3 = doubleNumberPickerType.getInt(R.styleable.QuadNumberPickerPreference_min3, 0);
+ mMax4 = doubleNumberPickerType.getInt(R.styleable.QuadNumberPickerPreference_max4, 5);
+ mMin4 = doubleNumberPickerType.getInt(R.styleable.QuadNumberPickerPreference_min4, 0);
+
+
+ mDefault1 = doubleNumberPickerType.getInt(R.styleable.QuadNumberPickerPreference_defaultValue1, mMin1);
+ mDefault2 = doubleNumberPickerType.getInt(R.styleable.QuadNumberPickerPreference_defaultValue2, mMin2);
+ mDefault3 = doubleNumberPickerType.getInt(R.styleable.QuadNumberPickerPreference_defaultValue3, mMin3);
+ mDefault4 = doubleNumberPickerType.getInt(R.styleable.QuadNumberPickerPreference_defaultValue4, mMin4);
+
+ // dialogType.recycle();
+ doubleNumberPickerType.recycle();
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.quad_number_picker_dialog, null);
+
+ mNumberPicker1 = (NumberPicker) view.findViewById(R.id.number_picker_1);
+ mNumberPicker2 = (NumberPicker) view.findViewById(R.id.number_picker_2);
+ mNumberPicker3 = (NumberPicker) view.findViewById(R.id.number_picker_3);
+ mNumberPicker4 = (NumberPicker) view.findViewById(R.id.number_picker_4);
+
+ if (mNumberPicker1 == null || mNumberPicker2 == null || mNumberPicker3 == null || mNumberPicker4 == null) {
+ throw new RuntimeException("mNumberPicker1 or mNumberPicker2 is null!");
+ }
+
+ // Initialize state
+ mNumberPicker1.setWrapSelectorWheel(false);
+ mNumberPicker1.setMaxValue(mMax1);
+ mNumberPicker1.setMinValue(mMin1);
+ mNumberPicker1.setValue(getPersistedValue(mDefault1));
+ mNumberPicker2.setWrapSelectorWheel(false);
+ mNumberPicker2.setMaxValue(mMax2);
+ mNumberPicker2.setMinValue(mMin2);
+ mNumberPicker1.setValue(getPersistedValue(mDefault2));
+ mNumberPicker3.setWrapSelectorWheel(false);
+ mNumberPicker3.setMaxValue(mMax3);
+ mNumberPicker3.setMinValue(mMin3);
+ mNumberPicker1.setValue(getPersistedValue(mDefault3));
+ mNumberPicker4.setWrapSelectorWheel(false);
+ mNumberPicker4.setMaxValue(mMax4);
+ mNumberPicker4.setMinValue(mMin4);
+ mNumberPicker1.setValue(getPersistedValue(mDefault4));
+
+ // Titles
+ TextView pickerTitle1 = (TextView) view.findViewById(R.id.picker_title_1);
+ TextView pickerTitle2 = (TextView) view.findViewById(R.id.picker_title_2);
+ TextView pickerTitle3 = (TextView) view.findViewById(R.id.picker_title_3);
+ TextView pickerTitle4 = (TextView) view.findViewById(R.id.picker_title_4);
+
+ if (pickerTitle1 != null && pickerTitle2 != null) {
+ pickerTitle1.setText(mPickerTitle1);
+ pickerTitle2.setText(mPickerTitle2);
+ pickerTitle3.setText(mPickerTitle3);
+ pickerTitle4.setText(mPickerTitle4);
+ }
+
+ // No keyboard popup
+ disableTextInput(mNumberPicker1);
+ disableTextInput(mNumberPicker2);
+ disableTextInput(mNumberPicker3);
+ disableTextInput(mNumberPicker4);
+ return view;
+ }
+
+ private int getPersistedValue(int value) {
+ String[] values = getPersistedString(mDefault1 + "|" + mDefault2 + "|" + mDefault3 + "|" + mDefault4).split("\\|");
+ if (value == 1) {
+ try {
+ return Integer.parseInt(values[0]);
+ } catch (NumberFormatException e) {
+ return mDefault1;
+ }
+ } else {
+ try {
+ return Integer.parseInt(values[1]);
+ } catch (NumberFormatException e) {
+ return mDefault2;
+ }
+ }
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ if (positiveResult) {
+ persistString(mNumberPicker1.getValue() + "|" + mNumberPicker2.getValue() + "|" + mNumberPicker3.getValue() + "|" + mNumberPicker4.getValue());
+ getOnPreferenceChangeListener().onPreferenceChange(this, mNumberPicker1.getValue() + "" + mNumberPicker2.getValue() + "" + mNumberPicker3.getValue() + "" + mNumberPicker4.getValue());
+ }
+ }
+
+ /*
+ * reflection of NumberPicker.java verified in 4.1, 4.2
+ */
+ private void disableTextInput(NumberPicker np) {
+ if (np == null)
+ return;
+ Class<?> classType = np.getClass();
+ Field inputTextField;
+ try {
+ inputTextField = classType.getDeclaredField("mInputText");
+ inputTextField.setAccessible(true);
+ EditText textInput = (EditText) inputTextField.get(np);
+ if (textInput != null) {
+ textInput.setCursorVisible(false);
+ textInput.setFocusable(false);
+ textInput.setFocusableInTouchMode(false);
+ }
+ } catch (Exception e) {
+ Log.d("QuadNumberPicker", "disableTextInput error", e);
+ }
+ }
+
+}
diff --git a/ring-android/src/cx/ring/views/SlidingUpPanelLayout.java b/ring-android/src/cx/ring/views/SlidingUpPanelLayout.java
new file mode 100644
index 0000000..d25b92e
--- /dev/null
+++ b/ring-android/src/cx/ring/views/SlidingUpPanelLayout.java
@@ -0,0 +1,1120 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.views;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.ViewDragHelper;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+public class SlidingUpPanelLayout extends ViewGroup {
+
+ private static final String TAG = SlidingUpPanelLayout.class.getSimpleName();
+
+ /**
+ * Default peeking out panel height
+ */
+ private static final int DEFAULT_PANEL_HEIGHT = 68; // dp;
+
+ /**
+ * Default height of the shadow above the peeking out panel
+ */
+ private static final int DEFAULT_SHADOW_HEIGHT = 4; // dp;
+
+ /**
+ * If no fade color is given by default it will fade to 80% gray.
+ */
+ private static final int DEFAULT_FADE_COLOR = 0x99000000;
+
+ /**
+ * Minimum velocity that will be detected as a fling
+ */
+ private static final int MIN_FLING_VELOCITY = 400; // dips per second
+
+ /**
+ * The fade color used for the panel covered by the slider. 0 = no fading.
+ */
+ private int mCoveredFadeColor = DEFAULT_FADE_COLOR;
+
+ /**
+ * The paint used to dim the main layout when sliding
+ */
+ private final Paint mCoveredFadePaint = new Paint();
+
+ /**
+ * Drawable used to draw the shadow between panes.
+ */
+ private Drawable mShadowDrawable;
+
+ /**
+ * The size of the overhang in pixels.
+ */
+ private int mPanelHeight;
+
+ /**
+ * The size of the shadow in pixels.
+ */
+ private final int mShadowHeight;
+
+ /**
+ * True if a panel can slide with the current measurements
+ */
+ private boolean mCanSlide;
+
+ /**
+ * If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be used for dragging.
+ */
+ private View mDragView;
+
+ /**
+ * The child view that can slide, if any.
+ */
+ private View mSlideableView;
+
+ /**
+ * How far the panel is offset from its expanded position. range [0, 1] where 0 = expanded, 1 = collapsed.
+ */
+ private float mSlideOffset;
+
+ /**
+ * How far in pixels the slideable panel may move.
+ */
+ private int mSlideRange;
+
+ /**
+ * A panel view is locked into internal scrolling or another condition that is preventing a drag.
+ */
+ private boolean mIsUnableToDrag;
+
+ /**
+ * Flag indicating that sliding feature is enabled\disabled
+ */
+ private boolean mIsSlidingEnabled;
+
+ /**
+ * Flag indicating if a drag view can have its own touch events. If set to true, a drag view can scroll horizontally and have its own click
+ * listener.
+ *
+ * Default is set to false.
+ */
+ private boolean mIsUsingDragViewTouchEvents;
+
+ /**
+ * Threshold to tell if there was a scroll touch event.
+ */
+ private int mScrollTouchSlop;
+
+ private float mInitialMotionX;
+ private float mInitialMotionY;
+ private boolean mDragViewHit;
+ private float mAnchorPoint = 0.f;
+
+ private PanelSlideListener mPanelSlideListener;
+
+ private final ViewDragHelper mDragHelper;
+
+ /**
+ * Stores whether or not the pane was expanded the last time it was slideable. If expand/collapse operations are invoked this state is modified.
+ * Used by instance state save/restore.
+ */
+ private boolean mPreservedExpandedState;
+ private boolean mFirstLayout = true;
+
+ private final Rect mTmpRect = new Rect();
+
+ /**
+ * Listener for monitoring events about sliding panes.
+ */
+ public interface PanelSlideListener {
+ /**
+ * Called when a sliding pane's position changes.
+ *
+ * @param panel
+ * The child view that was moved
+ * @param slideOffset
+ * The new offset of this sliding pane within its range, from 0-1
+ */
+ public void onPanelSlide(View panel, float slideOffset);
+
+ /**
+ * Called when a sliding pane becomes slid completely collapsed. The pane may or may not be interactive at this point depending on if it's
+ * shown or hidden
+ *
+ * @param panel
+ * The child view that was slid to an collapsed position, revealing other panes
+ */
+ public void onPanelCollapsed(View panel);
+
+ /**
+ * Called when a sliding pane becomes slid completely expanded. The pane is now guaranteed to be interactive. It may now obscure other views
+ * in the layout.
+ *
+ * @param panel
+ * The child view that was slid to a expanded position
+ */
+ public void onPanelExpanded(View panel);
+
+ public void onPanelAnchored(View panel);
+ }
+
+ /**
+ * No-op stubs for {@link PanelSlideListener}. If you only want to implement a subset of the listener methods you can extend this instead of
+ * implement the full interface.
+ */
+ public static class SimplePanelSlideListener implements PanelSlideListener {
+ @Override
+ public void onPanelSlide(View panel, float slideOffset) {
+ }
+
+ @Override
+ public void onPanelCollapsed(View panel) {
+ }
+
+ @Override
+ public void onPanelExpanded(View panel) {
+ }
+
+ @Override
+ public void onPanelAnchored(View panel) {
+ }
+ }
+
+ public SlidingUpPanelLayout(Context context) {
+ this(context, null);
+ }
+
+ public SlidingUpPanelLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SlidingUpPanelLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final float density = context.getResources().getDisplayMetrics().density;
+ mPanelHeight = (int) (DEFAULT_PANEL_HEIGHT * density + 0.5f);
+ mShadowHeight = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f);
+
+ setWillNotDraw(false);
+
+ mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback());
+ mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density);
+
+ mCanSlide = true;
+ mIsSlidingEnabled = true;
+
+ setCoveredFadeColor(DEFAULT_FADE_COLOR);
+
+ ViewConfiguration vc = ViewConfiguration.get(context);
+ mScrollTouchSlop = vc.getScaledTouchSlop();
+ }
+
+ /**
+ * Set the color used to fade the pane covered by the sliding pane out when the pane will become fully covered in the expanded state.
+ *
+ * @param color
+ * An ARGB-packed color value
+ */
+ public void setCoveredFadeColor(int color) {
+ mCoveredFadeColor = color;
+ invalidate();
+ }
+
+ /**
+ * @return The ARGB-packed color value used to fade the fixed pane
+ */
+ public int getCoveredFadeColor() {
+ return mCoveredFadeColor;
+ }
+
+ /**
+ * Set the collapsed panel height in pixels
+ *
+ * @param val
+ * A height in pixels
+ */
+ public void setPanelHeight(int val) {
+ mPanelHeight = val;
+ requestLayout();
+ }
+
+ /**
+ * @return The current collapsed panel height
+ */
+ public int getPanelHeight() {
+ return mPanelHeight;
+ }
+
+ public void setPanelSlideListener(PanelSlideListener listener) {
+ mPanelSlideListener = listener;
+ }
+
+ /**
+ * Set the draggable view portion. Use to null, to allow the whole panel to be draggable
+ *
+ * @param dragView
+ * A view that will be used to drag the panel.
+ */
+ public void setDragView(View dragView) {
+ mDragView = dragView;
+ }
+
+ /**
+ * Set an anchor point where the panel can stop during sliding
+ *
+ * @param anchorPoint
+ * A value between 0 and 1, determining the position of the anchor point starting from the top of the layout.
+ */
+ public void setAnchorPoint(float anchorPoint) {
+ if (anchorPoint > 0 && anchorPoint < 1)
+ mAnchorPoint = anchorPoint;
+ }
+
+ /**
+ * Set the shadow for the sliding panel
+ *
+ */
+ public void setShadowDrawable(Drawable drawable) {
+ mShadowDrawable = drawable;
+ }
+
+ void dispatchOnPanelSlide(View panel) {
+ if (mPanelSlideListener != null) {
+ mPanelSlideListener.onPanelSlide(panel, mSlideOffset);
+ }
+ }
+
+ void dispatchOnPanelExpanded(View panel) {
+ if (mPanelSlideListener != null) {
+ mPanelSlideListener.onPanelExpanded(panel);
+ }
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+
+ void dispatchOnPanelCollapsed(View panel) {
+ if (mPanelSlideListener != null) {
+ mPanelSlideListener.onPanelCollapsed(panel);
+ }
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+
+ void dispatchOnPanelAnchored(View panel) {
+ if (mPanelSlideListener != null) {
+ mPanelSlideListener.onPanelAnchored(panel);
+ }
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+
+ void updateObscuredViewVisibility() {
+ if (getChildCount() == 0) {
+ return;
+ }
+ final int leftBound = getPaddingLeft();
+ final int rightBound = getWidth() - getPaddingRight();
+ final int topBound = getPaddingTop();
+ final int bottomBound = getHeight() - getPaddingBottom();
+ final int left;
+ final int right;
+ final int top;
+ final int bottom;
+ if (mSlideableView != null && hasOpaqueBackground(mSlideableView)) {
+ left = mSlideableView.getLeft();
+ right = mSlideableView.getRight();
+ top = mSlideableView.getTop();
+ bottom = mSlideableView.getBottom();
+ } else {
+ left = right = top = bottom = 0;
+ }
+ View child = getChildAt(0);
+ final int clampedChildLeft = Math.max(leftBound, child.getLeft());
+ final int clampedChildTop = Math.max(topBound, child.getTop());
+ final int clampedChildRight = Math.min(rightBound, child.getRight());
+ final int clampedChildBottom = Math.min(bottomBound, child.getBottom());
+ final int vis;
+ if (clampedChildLeft >= left && clampedChildTop >= top && clampedChildRight <= right && clampedChildBottom <= bottom) {
+ vis = INVISIBLE;
+ } else {
+ vis = VISIBLE;
+ }
+ child.setVisibility(vis);
+ }
+
+ void setAllChildrenVisible() {
+ for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == INVISIBLE) {
+ child.setVisibility(VISIBLE);
+ }
+ }
+ }
+
+ private static boolean hasOpaqueBackground(View v) {
+ final Drawable bg = v.getBackground();
+ if (bg != null) {
+ return bg.getOpacity() == PixelFormat.OPAQUE;
+ }
+ return false;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mFirstLayout = true;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mFirstLayout = true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthMode != MeasureSpec.EXACTLY) {
+ throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
+ } else if (heightMode != MeasureSpec.EXACTLY) {
+ throw new IllegalStateException("Height must have an exact value or MATCH_PARENT");
+ }
+
+ int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
+ int panelHeight = mPanelHeight;
+
+ final int childCount = getChildCount();
+
+ if (childCount > 2) {
+ Log.e(TAG, "onMeasure: More than two child views are not supported.");
+ } else if (getChildAt(1).getVisibility() == GONE) {
+ panelHeight = 0;
+ }
+
+ // We'll find the current one below.
+ mSlideableView = null;
+ mCanSlide = false;
+
+ // First pass. Measure based on child LayoutParams width/height.
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ int height = layoutHeight;
+ if (child.getVisibility() == GONE) {
+ lp.dimWhenOffset = false;
+ continue;
+ }
+
+ if (i == 1) {
+ lp.slideable = true;
+ lp.dimWhenOffset = true;
+ mSlideableView = child;
+ mCanSlide = true;
+ } else {
+ height -= panelHeight;
+ }
+
+ int childWidthSpec;
+ if (lp.width == LayoutParams.WRAP_CONTENT) {
+ childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
+ } else if (lp.width == LayoutParams.MATCH_PARENT) {
+ childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+ } else {
+ childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
+ }
+
+ int childHeightSpec;
+ if (lp.height == LayoutParams.WRAP_CONTENT) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
+ } else if (lp.height == LayoutParams.MATCH_PARENT) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ } else {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+ }
+
+ child.measure(childWidthSpec, childHeightSpec);
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int paddingLeft = getPaddingLeft();
+ final int paddingTop = getPaddingTop();
+
+ final int childCount = getChildCount();
+ int yStart = paddingTop;
+ int nextYStart = yStart;
+
+ if (mFirstLayout) {
+ mSlideOffset = mCanSlide && mPreservedExpandedState ? 0.f : 1.f;
+ }
+
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ int childHeight = child.getMeasuredHeight();
+
+ if (lp.slideable) {
+ mSlideRange = childHeight - mPanelHeight;
+ yStart += (int) (mSlideRange * mSlideOffset);
+ } else {
+ yStart = nextYStart;
+ }
+
+ final int childTop = yStart;
+ final int childBottom = childTop + childHeight;
+ final int childLeft = paddingLeft;
+ final int childRight = childLeft + child.getMeasuredWidth();
+ child.layout(childLeft, childTop, childRight, childBottom);
+
+ nextYStart += child.getHeight();
+ }
+
+ if (mFirstLayout) {
+ updateObscuredViewVisibility();
+ }
+
+ mFirstLayout = false;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ // Recalculate sliding panes and their details
+ if (h != oldh) {
+ mFirstLayout = true;
+ }
+ }
+
+ /**
+ * Set sliding enabled flag
+ *
+ * @param enabled
+ * flag value
+ */
+ public void setSlidingEnabled(boolean enabled) {
+ mIsSlidingEnabled = enabled;
+ }
+
+ /**
+ * Set if the drag view can have its own touch events. If set to true, a drag view can scroll horizontally and have its own click listener.
+ *
+ * Default is set to false.
+ */
+ public void setEnableDragViewTouchEvents(boolean enabled) {
+ mIsUsingDragViewTouchEvents = enabled;
+ }
+
+ private boolean isDragViewHit(int x, int y) {
+ View v = mDragView != null ? mDragView : mSlideableView;
+ if (v == null)
+ return false;
+ int[] viewLocation = new int[2];
+ v.getLocationOnScreen(viewLocation);
+ int[] parentLocation = new int[2];
+ this.getLocationOnScreen(parentLocation);
+ int screenX = parentLocation[0] + x;
+ int screenY = parentLocation[1] + y;
+ return screenX >= viewLocation[0] && screenX < viewLocation[0] + v.getWidth() && screenY >= viewLocation[1]
+ && screenY < viewLocation[1] + v.getHeight();
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ super.requestChildFocus(child, focused);
+ if (!isInTouchMode() && !mCanSlide) {
+ mPreservedExpandedState = child == mSlideableView;
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ final int action = MotionEventCompat.getActionMasked(ev);
+
+ if (!mCanSlide || !mIsSlidingEnabled || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) {
+ mDragHelper.cancel();
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ mDragHelper.cancel();
+ return false;
+ }
+
+ final float x = ev.getX();
+ final float y = ev.getY();
+ boolean interceptTap = false;
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ mIsUnableToDrag = false;
+ mInitialMotionX = x;
+ mInitialMotionY = y;
+ mDragViewHit = isDragViewHit((int) x, (int) y);
+
+ if (mDragViewHit && !mIsUsingDragViewTouchEvents) {
+ interceptTap = true;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ final float adx = Math.abs(x - mInitialMotionX);
+ final float ady = Math.abs(y - mInitialMotionY);
+ final int dragSlop = mDragHelper.getTouchSlop();
+
+ // Handle any horizontal scrolling on the drag view.
+ if (mIsUsingDragViewTouchEvents) {
+ if (adx > mScrollTouchSlop && ady < mScrollTouchSlop) {
+ return super.onInterceptTouchEvent(ev);
+ }
+ // Intercept the touch if the drag view has any vertical scroll.
+ // onTouchEvent will determine if the view should drag vertically.
+ else if (ady > mScrollTouchSlop) {
+ interceptTap = mDragViewHit;
+ }
+ }
+
+ if (ady > dragSlop && adx > ady) {
+ mDragHelper.cancel();
+ mIsUnableToDrag = true;
+ return false;
+ }
+ break;
+ }
+ }
+
+ final boolean interceptForDrag = mDragViewHit && mDragHelper.shouldInterceptTouchEvent(ev);
+
+ return interceptForDrag || interceptTap;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (!mCanSlide || !mIsSlidingEnabled) {
+ return super.onTouchEvent(ev);
+ }
+
+ mDragHelper.processTouchEvent(ev);
+
+ final int action = ev.getAction();
+ boolean wantTouchEvents = true;
+
+ switch (action & MotionEventCompat.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ mInitialMotionX = x;
+ mInitialMotionY = y;
+ break;
+ }
+
+ case MotionEvent.ACTION_UP: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ final float dx = x - mInitialMotionX;
+ final float dy = y - mInitialMotionY;
+ final int slop = mDragHelper.getTouchSlop();
+ if (dx * dx + dy * dy < slop * slop && isDragViewHit((int) x, (int) y)) {
+ View v = mDragView != null ? mDragView : mSlideableView;
+ v.playSoundEffect(SoundEffectConstants.CLICK);
+ if (!isExpanded() && !isAnchored()) {
+ expandPane(mSlideableView, 0, mAnchorPoint);
+ } else {
+ collapsePane();
+ }
+ break;
+ }
+ break;
+ }
+ }
+
+ return wantTouchEvents;
+ }
+
+ private boolean expandPane(View pane, int initialVelocity, float mSlideOffset) {
+ if (mFirstLayout || smoothSlideTo(mSlideOffset, initialVelocity)) {
+ mPreservedExpandedState = true;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean collapsePane(View pane, int initialVelocity) {
+ if (mFirstLayout || smoothSlideTo(1.f, initialVelocity)) {
+ mPreservedExpandedState = false;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Collapse the sliding pane if it is currently slideable. If first layout has already completed this will animate.
+ *
+ * @return true if the pane was slideable and is now collapsed/in the process of collapsing
+ */
+ public boolean collapsePane() {
+ return collapsePane(mSlideableView, 0);
+ }
+
+ /**
+ * Expand the sliding pane if it is currently slideable. If first layout has already completed this will animate.
+ *
+ * @return true if the pane was slideable and is now expanded/in the process of expading
+ */
+ public boolean expandPane() {
+ return expandPane(0);
+ }
+
+ /**
+ * Partially expand the sliding pane up to a specific offset
+ *
+ * @param mSlideOffset
+ * Value between 0 and 1, where 0 is completely expanded.
+ * @return true if the pane was slideable and is now expanded/in the process of expading
+ */
+ public boolean expandPane(float mSlideOffset) {
+ if (!isPaneVisible()) {
+ showPane();
+ }
+ return expandPane(mSlideableView, 0, mSlideOffset);
+ }
+
+ /**
+ * Check if the layout is completely expanded.
+ *
+ * @return true if sliding panels are completely expanded
+ */
+ public boolean isExpanded() {
+ return mFirstLayout && mPreservedExpandedState || !mFirstLayout && mCanSlide && mSlideOffset == 0;
+ }
+
+ /**
+ * Check if the layout is anchored in an intermediate point.
+ *
+ * @return true if sliding panels are anchored
+ */
+ public boolean isAnchored() {
+ int anchoredTop = (int) (mAnchorPoint * mSlideRange);
+ return !mFirstLayout && mCanSlide && mSlideOffset == (float) anchoredTop / (float) mSlideRange;
+ }
+
+ /**
+ * Check if the content in this layout cannot fully fit side by side and therefore the content pane can be slid back and forth.
+ *
+ * @return true if content in this layout can be expanded
+ */
+ public boolean isSlideable() {
+ return mCanSlide;
+ }
+
+ public boolean isPaneVisible() {
+ if (getChildCount() < 2) {
+ return false;
+ }
+ View slidingPane = getChildAt(1);
+ return slidingPane.getVisibility() == View.VISIBLE;
+ }
+
+ public void showPane() {
+ if (getChildCount() < 2) {
+ return;
+ }
+ View slidingPane = getChildAt(1);
+ slidingPane.setVisibility(View.VISIBLE);
+ requestLayout();
+ }
+
+ public void hidePane() {
+ if (mSlideableView == null) {
+ return;
+ }
+ mSlideableView.setVisibility(View.GONE);
+ requestLayout();
+ }
+
+ private void onPanelDragged(int newTop) {
+ final int topBound = getPaddingTop();
+ mSlideOffset = (float) (newTop - topBound) / mSlideRange;
+ dispatchOnPanelSlide(mSlideableView);
+ }
+
+ @Override
+ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ boolean result;
+ final int save = canvas.save(Canvas.CLIP_SAVE_FLAG);
+
+ boolean drawScrim = false;
+
+ if (mCanSlide && !lp.slideable && mSlideableView != null) {
+ // Clip against the slider; no sense drawing what will immediately be covered.
+ canvas.getClipBounds(mTmpRect);
+ mTmpRect.bottom = (int) Math.min(mTmpRect.bottom, mSlideableView.getTop() + getResources().getDisplayMetrics().density * 68); // + 60
+ // cause of
+ // the
+ // rounded
+ // shape
+ // handle
+ canvas.clipRect(mTmpRect);
+ if (mSlideOffset < 1) {
+ drawScrim = true;
+ }
+ }
+
+ result = super.drawChild(canvas, child, drawingTime);
+ canvas.restoreToCount(save);
+
+ if (drawScrim) {
+ final int baseAlpha = (mCoveredFadeColor & 0xff000000) >>> 24;
+ final int imag = (int) (baseAlpha * (1 - mSlideOffset));
+ final int color = imag << 24 | (mCoveredFadeColor & 0xffffff);
+ mCoveredFadePaint.setColor(color);
+ canvas.drawRect(mTmpRect, mCoveredFadePaint);
+ }
+
+ return result;
+ }
+
+ /**
+ * Smoothly animate mDraggingPane to the target X position within its range.
+ *
+ * @param slideOffset
+ * position to animate to
+ * @param velocity
+ * initial velocity in case of fling, or 0.
+ */
+ boolean smoothSlideTo(float slideOffset, int velocity) {
+ if (!mCanSlide) {
+ // Nothing to do.
+ return false;
+ }
+
+ final int topBound = getPaddingTop();
+ int y = (int) (topBound + slideOffset * mSlideRange);
+
+ if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), y)) {
+ setAllChildrenVisible();
+ ViewCompat.postInvalidateOnAnimation(this);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mDragHelper.continueSettling(true)) {
+ if (!mCanSlide) {
+ mDragHelper.abort();
+ return;
+ }
+
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+
+ @Override
+ public void draw(Canvas c) {
+ super.draw(c);
+
+ if (mSlideableView == null) {
+ // No need to draw a shadow if we don't have one.
+ return;
+ }
+
+ final int right = mSlideableView.getRight();
+ final int top = mSlideableView.getTop() - mShadowHeight;
+ final int bottom = mSlideableView.getTop();
+ final int left = mSlideableView.getLeft();
+
+ if (mShadowDrawable != null) {
+ mShadowDrawable.setBounds(left, top, right, bottom);
+ mShadowDrawable.draw(c);
+ }
+ }
+
+ /**
+ * Tests scrollability within child views of v given a delta of dx.
+ *
+ * @param v
+ * View to test for horizontal scrollability
+ * @param checkV
+ * Whether the view v passed should itself be checked for scrollability (true), or just its children (false).
+ * @param dx
+ * Delta scrolled in pixels
+ * @param x
+ * X coordinate of the active touch point
+ * @param y
+ * Y coordinate of the active touch point
+ * @return true if child views of v can be scrolled by delta of dx.
+ */
+ protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
+ if (v instanceof ViewGroup) {
+ final ViewGroup group = (ViewGroup) v;
+ final int scrollX = v.getScrollX();
+ final int scrollY = v.getScrollY();
+ final int count = group.getChildCount();
+ // Count backwards - let topmost views consume scroll distance first.
+ for (int i = count - 1; i >= 0; i--) {
+ final View child = group.getChildAt(i);
+ if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop()
+ && y + scrollY < child.getBottom() && canScroll(child, true, dx, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) {
+ return true;
+ }
+ }
+ }
+ return checkV && ViewCompat.canScrollHorizontally(v, -dx);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams();
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : new LayoutParams(p);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams && super.checkLayoutParams(p);
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ SavedState ss = new SavedState(superState);
+ ss.isExpanded = isSlideable() ? isExpanded() : mPreservedExpandedState;
+
+ return ss;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ if (ss.isExpanded) {
+ expandPane();
+ } else {
+ collapsePane();
+ }
+ mPreservedExpandedState = ss.isExpanded;
+ }
+
+ private class DragHelperCallback extends ViewDragHelper.Callback {
+
+ @Override
+ public boolean tryCaptureView(View child, int pointerId) {
+ if (mIsUnableToDrag) {
+ return false;
+ }
+
+ return ((LayoutParams) child.getLayoutParams()).slideable;
+ }
+
+ @Override
+ public void onViewDragStateChanged(int state) {
+ if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
+ if (mSlideOffset == 0) {
+ updateObscuredViewVisibility();
+ dispatchOnPanelExpanded(mSlideableView);
+ mPreservedExpandedState = true;
+ } else if (isAnchored()) {
+ updateObscuredViewVisibility();
+ dispatchOnPanelAnchored(mSlideableView);
+ mPreservedExpandedState = true;
+ } else {
+ dispatchOnPanelCollapsed(mSlideableView);
+ mPreservedExpandedState = false;
+ }
+ }
+ }
+
+ @Override
+ public void onViewCaptured(View capturedChild, int activePointerId) {
+ // Make all child views visible in preparation for sliding things around
+ setAllChildrenVisible();
+ }
+
+ @Override
+ public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+ onPanelDragged(top);
+ invalidate();
+ }
+
+ @Override
+ public void onViewReleased(View releasedChild, float xvel, float yvel) {
+ int top = getPaddingTop();
+
+ if (mAnchorPoint != 0) {
+ int anchoredTop = (int) (mAnchorPoint * mSlideRange);
+ float anchorOffset = (float) anchoredTop / (float) mSlideRange;
+
+ if (yvel > 0 || (yvel == 0 && mSlideOffset >= (1f + anchorOffset) / 2)) {
+ top += mSlideRange;
+ } else if (yvel == 0 && mSlideOffset < (1f + anchorOffset) / 2 && mSlideOffset >= anchorOffset / 2) {
+ top += mSlideRange * mAnchorPoint;
+ }
+
+ } else if (yvel > 0 || (yvel == 0 && mSlideOffset > 0.5f)) {
+ top += mSlideRange;
+ }
+
+ mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
+ invalidate();
+ }
+
+ @Override
+ public int getViewVerticalDragRange(View child) {
+ return mSlideRange;
+ }
+
+ @Override
+ public int clampViewPositionVertical(View child, int top, int dy) {
+ final int topBound = getPaddingTop();
+ final int bottomBound = topBound + mSlideRange;
+
+ final int newLeft = Math.min(Math.max(top, topBound), bottomBound);
+
+ return newLeft;
+ }
+
+ }
+
+ public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+ private static final int[] ATTRS = new int[] { android.R.attr.layout_weight };
+
+ /**
+ * True if this pane is the slideable pane in the layout.
+ */
+ boolean slideable;
+
+ /**
+ * True if this view should be drawn dimmed when it's been offset from its default position.
+ */
+ boolean dimWhenOffset;
+
+ Paint dimPaint;
+
+ public LayoutParams() {
+ super(MATCH_PARENT, MATCH_PARENT);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(android.view.ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS);
+ a.recycle();
+ }
+
+ }
+
+ static class SavedState extends BaseSavedState {
+ boolean isExpanded;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ isExpanded = in.readInt() != 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(isExpanded ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
diff --git a/ring-android/src/cx/ring/views/SwipeListViewTouchListener.java b/ring-android/src/cx/ring/views/SwipeListViewTouchListener.java
new file mode 100644
index 0000000..8e05466
--- /dev/null
+++ b/ring-android/src/cx/ring/views/SwipeListViewTouchListener.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package cx.ring.views;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import cx.ring.R;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.ListView;
+
+public class SwipeListViewTouchListener implements View.OnTouchListener {
+ // Cached ViewConfiguration and system-wide constant values
+ private int mSlop;
+ private int mMinFlingVelocity;
+ private int mMaxFlingVelocity;
+ private long mAnimationTime;
+
+ private static final String TAG = SwipeListViewTouchListener.class.getSimpleName();
+
+ // Fixed properties
+ private ListView mListView;
+ private OnSwipeCallback mCallback;
+ private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero
+ private boolean dismissLeft = true;
+ private boolean dismissRight = true;
+
+ // Transient properties
+ private List<PendingSwipeData> mPendingSwipes = new ArrayList<PendingSwipeData>();
+ private int mDismissAnimationRefCount = 0;
+ private float mDownX;
+ private float mDownY;
+ private boolean mSwiping;
+ private VelocityTracker mVelocityTracker;
+ private int mDownPosition;
+ private View mDownView, mUnderDownView;
+ private boolean mPaused;
+
+ /**
+ * The callback interface used by {@link SwipeListViewTouchListener} to inform its client about a successful swipe of one or more list item
+ * positions.
+ */
+ public interface OnSwipeCallback {
+ /**
+ * Called when the user has swiped the list item to the left.
+ *
+ * @param listView
+ * The originating {@link ListView}.
+ * @param reverseSortedPositions
+ * An array of positions to dismiss, sorted in descending order for convenience.
+ */
+ void onSwipeLeft(ListView listView, int[] reverseSortedPositions);
+
+ void onSwipeRight(ListView listView, View downView);
+ }
+
+ /**
+ * Constructs a new swipe-to-action touch listener for the given list view.
+ *
+ * @param listView
+ * The list view whose items should be dismissable.
+ * @param callback
+ * The callback to trigger when the user has indicated that she would like to dismiss one or more list items.
+ */
+ public SwipeListViewTouchListener(ListView listView, OnSwipeCallback callback) {
+ ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
+ mSlop = vc.getScaledTouchSlop();
+ mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+ mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
+ mAnimationTime = listView.getContext().getResources().getInteger(android.R.integer.config_shortAnimTime);
+ mListView = listView;
+ mCallback = callback;
+ }
+
+ /**
+ * Constructs a new swipe-to-action touch listener for the given list view.
+ *
+ * @param listView
+ * The list view whose items should be dismissable.
+ * @param callback
+ * The callback to trigger when the user has indicated that she would like to dismiss one or more list items.
+ * @param dismissLeft
+ * set if the dismiss animation is up when the user swipe to the left
+ * @param dismissRight
+ * set if the dismiss animation is up when the user swipe to the right
+ * @see #SwipeListViewTouchListener(ListView, OnSwipeCallback, boolean, boolean)
+ */
+ public SwipeListViewTouchListener(ListView listView, OnSwipeCallback callback, boolean dismissLeft, boolean dismissRight) {
+ this(listView, callback);
+ this.dismissLeft = dismissLeft;
+ this.dismissRight = dismissRight;
+ }
+
+ /**
+ * Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures.
+ *
+ * @param enabled
+ * Whether or not to watch for gestures.
+ */
+ public void setEnabled(boolean enabled) {
+ mPaused = !enabled;
+ }
+
+ /**
+ * Returns an {@link android.widget.AbsListView.OnScrollListener} to be added to the {@link ListView} using
+ * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}. If a scroll listener is already assigned, the caller should
+ * still pass scroll changes through to this listener. This will ensure that this {@link SwipeListViewTouchListener} is paused during list view
+ * scrolling.</p>
+ *
+ * @see {@link SwipeListViewTouchListener}
+ */
+ public AbsListView.OnScrollListener makeScrollListener() {
+ return new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView absListView, int scrollState) {
+ setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+ }
+
+ @Override
+ public void onScroll(AbsListView absListView, int i, int i1, int i2) {
+ }
+ };
+ }
+
+ @Override
+ public boolean onTouch(View item, MotionEvent motionEvent) {
+ if (mViewWidth < 2) {
+ mViewWidth = mListView.getWidth();
+ }
+
+ switch (motionEvent.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ if (mPaused) {
+ return false;
+ }
+
+ // TODO: ensure this is a finger, and set a flag
+
+ // Find the child view that was touched (perform a hit test)
+ Rect rect = new Rect();
+ int childCount = mListView.getChildCount();
+ int[] listViewCoords = new int[2];
+ mListView.getLocationOnScreen(listViewCoords);
+ int x = (int) motionEvent.getRawX() - listViewCoords[0];
+ int y = (int) motionEvent.getRawY() - listViewCoords[1];
+ View child;
+ for (int i = 0; i < childCount; i++) {
+ child = mListView.getChildAt(i);
+ child.getHitRect(rect);
+ if (rect.contains(x, y)) {
+ mDownView = child.findViewById(R.id.contactview);
+ mUnderDownView = child.findViewById(R.id.contact_underview);
+ break;
+ }
+ }
+
+ if (mDownView != null) {
+
+ mDownX = motionEvent.getRawX() - mDownView.getTranslationX();
+ mDownY = motionEvent.getRawY();
+ mDownPosition = mListView.getPositionForView(mDownView);
+
+ mVelocityTracker = VelocityTracker.obtain();
+ mVelocityTracker.addMovement(motionEvent);
+ }
+ item.onTouchEvent(motionEvent);
+ return true;
+ }
+
+ case MotionEvent.ACTION_UP: {
+ if (mVelocityTracker == null) {
+ break;
+ }
+
+ float deltaX = motionEvent.getRawX() - mDownX;
+
+ mVelocityTracker.addMovement(motionEvent);
+ mVelocityTracker.computeCurrentVelocity(500); // 1000 by defaut but it was too much
+ float velocityX = Math.abs(mVelocityTracker.getXVelocity());
+ float velocityY = Math.abs(mVelocityTracker.getYVelocity());
+ boolean swipe = false;
+ boolean swipeRight = false;
+
+ if (mDownView.getTranslationX() > mViewWidth / 2) {
+ swipe = true;
+ swipeRight = deltaX > 0;
+ } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity && velocityY < velocityX) {
+ swipe = true;
+ swipeRight = mVelocityTracker.getXVelocity() > 0;
+ }
+ if (swipe) {
+ // sufficent swipe value
+ final View downView = mDownView; // mDownView gets null'd before animation ends
+ final int downPosition = mDownPosition;
+ final boolean toTheRight = swipeRight;
+ ++mDismissAnimationRefCount;
+
+ if (toTheRight) {
+ mDownView.animate().translationX(mViewWidth / 2).alpha(1).setDuration(mAnimationTime).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mListView.requestDisallowInterceptTouchEvent(false);
+ // mCallback.onSwipeRight(mListView, mUnderDownView);
+ toggleUnderLayerState(true);
+ // performSwipeAction(downView, downPosition, toTheRight,dismissRight);
+ }
+ });
+ } else {
+ mDownView.animate().translationX(0).alpha(1).setDuration(mAnimationTime);
+ }
+
+ } else {
+ // cancel
+ mDownView.animate().translationX(0).alpha(1).setDuration(mAnimationTime).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ toggleUnderLayerState(false);
+ mUnderDownView = null;
+ }
+ });
+ }
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ mDownX = 0;
+ mDownView = null;
+ mDownPosition = ListView.INVALID_POSITION;
+ mSwiping = false;
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ if (mVelocityTracker == null || mPaused) {
+ break;
+ }
+
+ mVelocityTracker.addMovement(motionEvent);
+ mVelocityTracker.computeCurrentVelocity(500);
+ float deltaX = motionEvent.getRawX() - mDownX;
+ float deltaY = motionEvent.getRawY() - mDownY;
+
+ if (Math.abs(deltaX) < Math.abs(deltaY)) {
+ mListView.requestDisallowInterceptTouchEvent(false);
+ return false;
+ }
+
+ if (Math.abs(deltaX) > mSlop) {
+ mSwiping = true;
+ mListView.requestDisallowInterceptTouchEvent(true);
+
+ // Cancel ListView's touch (un-highlighting the item)
+ MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
+ cancelEvent.setAction(MotionEvent.ACTION_CANCEL | (motionEvent.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
+ mListView.onTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ }
+ if (deltaX < 0)
+ return true;
+
+ if (mSwiping) {
+ mDownView.setTranslationX(deltaX);
+ // mDownView.setAlpha(Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / mViewWidth)));
+ return true;
+ }
+ break;
+ }
+ }
+ return false;
+ }
+
+ private void toggleUnderLayerState(boolean b) {
+ if (mUnderDownView == null)
+ return;
+ mUnderDownView.findViewById(R.id.quick_edit).setClickable(b);
+ mUnderDownView.findViewById(R.id.quick_discard).setClickable(b);
+ mUnderDownView.findViewById(R.id.quick_starred).setClickable(b);
+ }
+
+ class PendingSwipeData implements Comparable<PendingSwipeData> {
+ public int position;
+ public View view;
+
+ public PendingSwipeData(int position, View view) {
+ this.position = position;
+ this.view = view;
+ }
+
+ @Override
+ public int compareTo(PendingSwipeData other) {
+ // Sort by descending position
+ return other.position - position;
+ }
+ }
+
+ private void performSwipeAction(final View swipeView, final int swipePosition, boolean toTheRight, boolean dismiss) {
+ // Animate the dismissed list item to zero-height and fire the dismiss callback when
+ // all dismissed list item animations have completed. This triggers layout on each animation
+ // frame; in the future we may want to do something smarter and more performant.
+
+ final ViewGroup.LayoutParams lp = swipeView.getLayoutParams();
+ final int originalHeight = swipeView.getHeight();
+ final boolean swipeRight = toTheRight;
+
+ ValueAnimator animator;
+ if (dismiss)
+ animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(mAnimationTime);
+ else
+ animator = ValueAnimator.ofInt(originalHeight, originalHeight - 1).setDuration(mAnimationTime);
+
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ --mDismissAnimationRefCount;
+ if (mDismissAnimationRefCount == 0) {
+ // No active animations, process all pending dismisses.
+ // Sort by descending position
+ Collections.sort(mPendingSwipes);
+
+ int[] swipePositions = new int[mPendingSwipes.size()];
+ for (int i = mPendingSwipes.size() - 1; i >= 0; i--) {
+ swipePositions[i] = mPendingSwipes.get(i).position;
+ }
+ // if (swipeRight)
+ // mCallback.onSwipeRight(mListView, swipePositions);
+ // else
+ // mCallback.onSwipeLeft(mListView, swipePositions);
+
+ ViewGroup.LayoutParams lp;
+ for (PendingSwipeData pendingDismiss : mPendingSwipes) {
+ // Reset view presentation
+ pendingDismiss.view.setAlpha(1f);
+ pendingDismiss.view.setTranslationX(0);
+ lp = pendingDismiss.view.getLayoutParams();
+ lp.height = originalHeight;
+ pendingDismiss.view.setLayoutParams(lp);
+ }
+
+ mPendingSwipes.clear();
+ }
+ }
+ });
+
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ lp.height = (Integer) valueAnimator.getAnimatedValue();
+ swipeView.setLayoutParams(lp);
+ }
+ });
+
+ mPendingSwipes.add(new PendingSwipeData(swipePosition, swipeView));
+ animator.start();
+ }
+
+ public void openItem(View child, int pos, long id) {
+
+ mDownView = child.findViewById(R.id.contactview);
+ mUnderDownView = child.findViewById(R.id.contact_underview);
+ if (mDownView.getTranslationX() > 0)
+ return;
+ mDownView.animate().translationX(mViewWidth / 2).setDuration(mAnimationTime).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mListView.requestDisallowInterceptTouchEvent(false);
+ toggleUnderLayerState(true);
+ // performSwipeAction(downView, downPosition, toTheRight,dismissRight);
+ }
+ });
+ }
+}
diff --git a/ring-android/src/cx/ring/views/dragsortlv/DragSortController.java b/ring-android/src/cx/ring/views/dragsortlv/DragSortController.java
new file mode 100644
index 0000000..616c6ac
--- /dev/null
+++ b/ring-android/src/cx/ring/views/dragsortlv/DragSortController.java
@@ -0,0 +1,508 @@
+/*
+ * Copyright (C) 2004-2013 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ *
+ * Part of the library:
+ * https://github.com/bauerca/drag-sort-listview
+ * No longer maintained
+ *
+ *
+ */
+
+
+
+
+package cx.ring.views.dragsortlv;
+
+import android.graphics.Point;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.AdapterView;
+
+/**
+ * Class that starts and stops item drags on a {@link DragSortListView}
+ * based on touch gestures. This class also inherits from
+ * {@link SimpleFloatViewManager}, which provides basic float View
+ * creation.
+ *
+ * An instance of this class is meant to be passed to the methods
+ * {@link DragSortListView#setTouchListener()} and
+ * {@link DragSortListView#setFloatViewManager()} of your
+ * {@link DragSortListView} instance.
+ */
+public class DragSortController extends SimpleFloatViewManager implements View.OnTouchListener, GestureDetector.OnGestureListener {
+
+ /**
+ * Drag init mode enum.
+ */
+ public static final int ON_DOWN = 0;
+ public static final int ON_DRAG = 1;
+ public static final int ON_LONG_PRESS = 2;
+
+ private int mDragInitMode = ON_DOWN;
+
+ private boolean mSortEnabled = true;
+
+ /**
+ * Remove mode enum.
+ */
+ public static final int CLICK_REMOVE = 0;
+ public static final int FLING_REMOVE = 1;
+
+ /**
+ * The current remove mode.
+ */
+ private int mRemoveMode;
+
+ private boolean mRemoveEnabled = false;
+ private boolean mIsRemoving = false;
+
+ private GestureDetector mDetector;
+
+ private GestureDetector mFlingRemoveDetector;
+
+ private int mTouchSlop;
+
+ public static final int MISS = -1;
+
+ private int mHitPos = MISS;
+ private int mFlingHitPos = MISS;
+
+ private int mClickRemoveHitPos = MISS;
+
+ private int[] mTempLoc = new int[2];
+
+ private int mItemX;
+ private int mItemY;
+
+ private int mCurrX;
+ private int mCurrY;
+
+ private boolean mDragging = false;
+
+ private float mFlingSpeed = 500f;
+
+ private int mDragHandleId;
+
+ private int mClickRemoveId;
+
+ private int mFlingHandleId;
+ private boolean mCanDrag;
+
+ private DragSortListView mDslv;
+ private int mPositionX;
+
+ /**
+ * Calls {@link #DragSortController(DragSortListView, int)} with a
+ * 0 drag handle id, FLING_RIGHT_REMOVE remove mode,
+ * and ON_DOWN drag init. By default, sorting is enabled, and
+ * removal is disabled.
+ *
+ * @param dslv The DSLV instance
+ */
+ public DragSortController(DragSortListView dslv) {
+ this(dslv, 0, ON_DOWN, FLING_REMOVE);
+ }
+
+ public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode) {
+ this(dslv, dragHandleId, dragInitMode, removeMode, 0);
+ }
+
+ public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode, int clickRemoveId) {
+ this(dslv, dragHandleId, dragInitMode, removeMode, clickRemoveId, 0);
+ }
+
+ /**
+ * By default, sorting is enabled, and removal is disabled.
+ *
+ * @param dslv The DSLV instance
+ * @param dragHandleId The resource id of the View that represents
+ * the drag handle in a list item.
+ */
+ public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode,
+ int removeMode, int clickRemoveId, int flingHandleId) {
+ super(dslv);
+ mDslv = dslv;
+ mDetector = new GestureDetector(dslv.getContext(), this);
+ mFlingRemoveDetector = new GestureDetector(dslv.getContext(), mFlingRemoveListener);
+ mFlingRemoveDetector.setIsLongpressEnabled(false);
+ mTouchSlop = ViewConfiguration.get(dslv.getContext()).getScaledTouchSlop();
+ mDragHandleId = dragHandleId;
+ mClickRemoveId = clickRemoveId;
+ mFlingHandleId = flingHandleId;
+ setRemoveMode(removeMode);
+ setDragInitMode(dragInitMode);
+ }
+
+
+ public int getDragInitMode() {
+ return mDragInitMode;
+ }
+
+ /**
+ * Set how a drag is initiated. Needs to be one of
+ * {@link ON_DOWN}, {@link ON_DRAG}, or {@link ON_LONG_PRESS}.
+ *
+ * @param mode The drag init mode.
+ */
+ public void setDragInitMode(int mode) {
+ mDragInitMode = mode;
+ }
+
+ /**
+ * Enable/Disable list item sorting. Disabling is useful if only item
+ * removal is desired. Prevents drags in the vertical direction.
+ *
+ * @param enabled Set <code>true</code> to enable list
+ * item sorting.
+ */
+ public void setSortEnabled(boolean enabled) {
+ mSortEnabled = enabled;
+ }
+
+ public boolean isSortEnabled() {
+ return mSortEnabled;
+ }
+
+ /**
+ * One of {@link CLICK_REMOVE}, {@link FLING_RIGHT_REMOVE},
+ * {@link FLING_LEFT_REMOVE},
+ * {@link SLIDE_RIGHT_REMOVE}, or {@link SLIDE_LEFT_REMOVE}.
+ */
+ public void setRemoveMode(int mode) {
+ mRemoveMode = mode;
+ }
+
+ public int getRemoveMode() {
+ return mRemoveMode;
+ }
+
+ /**
+ * Enable/Disable item removal without affecting remove mode.
+ */
+ public void setRemoveEnabled(boolean enabled) {
+ mRemoveEnabled = enabled;
+ }
+
+ public boolean isRemoveEnabled() {
+ return mRemoveEnabled;
+ }
+
+ /**
+ * Set the resource id for the View that represents the drag
+ * handle in a list item.
+ *
+ * @param id An android resource id.
+ */
+ public void setDragHandleId(int id) {
+ mDragHandleId = id;
+ }
+
+ /**
+ * Set the resource id for the View that represents the fling
+ * handle in a list item.
+ *
+ * @param id An android resource id.
+ */
+ public void setFlingHandleId(int id) {
+ mFlingHandleId = id;
+ }
+
+ /**
+ * Set the resource id for the View that represents click
+ * removal button.
+ *
+ * @param id An android resource id.
+ */
+ public void setClickRemoveId(int id) {
+ mClickRemoveId = id;
+ }
+
+ /**
+ * Sets flags to restrict certain motions of the floating View
+ * based on DragSortController settings (such as remove mode).
+ * Starts the drag on the DragSortListView.
+ *
+ * @param position The list item position (includes headers).
+ * @param deltaX Touch x-coord minus left edge of floating View.
+ * @param deltaY Touch y-coord minus top edge of floating View.
+ *
+ * @return True if drag started, false otherwise.
+ */
+ public boolean startDrag(int position, int deltaX, int deltaY) {
+
+ int dragFlags = 0;
+ if (mSortEnabled && !mIsRemoving) {
+ dragFlags |= DragSortListView.DRAG_POS_Y | DragSortListView.DRAG_NEG_Y;
+ }
+ if (mRemoveEnabled && mIsRemoving) {
+ dragFlags |= DragSortListView.DRAG_POS_X;
+ dragFlags |= DragSortListView.DRAG_NEG_X;
+ }
+
+ mDragging = mDslv.startDrag(position - mDslv.getHeaderViewsCount(), dragFlags, deltaX,
+ deltaY);
+ return mDragging;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent ev) {
+ if (!mDslv.isDragEnabled() || mDslv.listViewIntercepted()) {
+ return false;
+ }
+
+ mDetector.onTouchEvent(ev);
+ if (mRemoveEnabled && mDragging && mRemoveMode == FLING_REMOVE) {
+ mFlingRemoveDetector.onTouchEvent(ev);
+ }
+
+ int action = ev.getAction() & MotionEvent.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mCurrX = (int) ev.getX();
+ mCurrY = (int) ev.getY();
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mRemoveEnabled && mIsRemoving) {
+ int x = mPositionX >= 0 ? mPositionX : -mPositionX;
+ int removePoint = mDslv.getWidth() / 2;
+ if (x > removePoint) {
+ mDslv.stopDragWithVelocity(true, 0);
+ }
+ }
+ case MotionEvent.ACTION_CANCEL:
+ mIsRemoving = false;
+ mDragging = false;
+ break;
+ }
+
+ return false;
+ }
+
+ /**
+ * Overrides to provide fading when slide removal is enabled.
+ */
+ @Override
+ public void onDragFloatView(View floatView, Point position, Point touch) {
+
+ if (mRemoveEnabled && mIsRemoving) {
+ mPositionX = position.x;
+ }
+ }
+
+ /**
+ * Get the position to start dragging based on the ACTION_DOWN
+ * MotionEvent. This function simply calls
+ * {@link #dragHandleHitPosition(MotionEvent)}. Override
+ * to change drag handle behavior;
+ * this function is called internally when an ACTION_DOWN
+ * event is detected.
+ *
+ * @param ev The ACTION_DOWN MotionEvent.
+ *
+ * @return The list position to drag if a drag-init gesture is
+ * detected; MISS if unsuccessful.
+ */
+ public int startDragPosition(MotionEvent ev) {
+ return dragHandleHitPosition(ev);
+ }
+
+ public int startFlingPosition(MotionEvent ev) {
+ return mRemoveMode == FLING_REMOVE ? flingHandleHitPosition(ev) : MISS;
+ }
+
+ /**
+ * Checks for the touch of an item's drag handle (specified by
+ * {@link #setDragHandleId(int)}), and returns that item's position
+ * if a drag handle touch was detected.
+ *
+ * @param ev The ACTION_DOWN MotionEvent.
+
+ * @return The list position of the item whose drag handle was
+ * touched; MISS if unsuccessful.
+ */
+ public int dragHandleHitPosition(MotionEvent ev) {
+ return viewIdHitPosition(ev, mDragHandleId);
+ }
+
+ public int flingHandleHitPosition(MotionEvent ev) {
+ return viewIdHitPosition(ev, mFlingHandleId);
+ }
+
+ public int viewIdHitPosition(MotionEvent ev, int id) {
+ final int x = (int) ev.getX();
+ final int y = (int) ev.getY();
+
+ int touchPos = mDslv.pointToPosition(x, y); // includes headers/footers
+
+ final int numHeaders = mDslv.getHeaderViewsCount();
+ final int numFooters = mDslv.getFooterViewsCount();
+ final int count = mDslv.getCount();
+
+ // Log.d("mobeta", "touch down on position " + itemnum);
+ // We're only interested if the touch was on an
+ // item that's not a header or footer.
+ if (touchPos != AdapterView.INVALID_POSITION && touchPos >= numHeaders
+ && touchPos < (count - numFooters)) {
+ final View item = mDslv.getChildAt(touchPos - mDslv.getFirstVisiblePosition());
+ final int rawX = (int) ev.getRawX();
+ final int rawY = (int) ev.getRawY();
+
+ View dragBox = id == 0 ? item : (View) item.findViewById(id);
+ if (dragBox != null) {
+ dragBox.getLocationOnScreen(mTempLoc);
+
+ if (rawX > mTempLoc[0] && rawY > mTempLoc[1] &&
+ rawX < mTempLoc[0] + dragBox.getWidth() &&
+ rawY < mTempLoc[1] + dragBox.getHeight()) {
+
+ mItemX = item.getLeft();
+ mItemY = item.getTop();
+
+ return touchPos;
+ }
+ }
+ }
+
+ return MISS;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent ev) {
+ if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
+ mClickRemoveHitPos = viewIdHitPosition(ev, mClickRemoveId);
+ }
+
+ mHitPos = startDragPosition(ev);
+ if (mHitPos != MISS && mDragInitMode == ON_DOWN) {
+ startDrag(mHitPos, (int) ev.getX() - mItemX, (int) ev.getY() - mItemY);
+ }
+
+ mIsRemoving = false;
+ mCanDrag = true;
+ mPositionX = 0;
+ mFlingHitPos = startFlingPosition(ev);
+
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+
+ final int x1 = (int) e1.getX();
+ final int y1 = (int) e1.getY();
+ final int x2 = (int) e2.getX();
+ final int y2 = (int) e2.getY();
+ final int deltaX = x2 - mItemX;
+ final int deltaY = y2 - mItemY;
+
+ if (mCanDrag && !mDragging && (mHitPos != MISS || mFlingHitPos != MISS)) {
+ if (mHitPos != MISS) {
+ if (mDragInitMode == ON_DRAG && Math.abs(y2 - y1) > mTouchSlop && mSortEnabled) {
+ startDrag(mHitPos, deltaX, deltaY);
+ }
+ else if (mDragInitMode != ON_DOWN && Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled)
+ {
+ mIsRemoving = true;
+ startDrag(mFlingHitPos, deltaX, deltaY);
+ }
+ } else if (mFlingHitPos != MISS) {
+ if (Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled) {
+ mIsRemoving = true;
+ startDrag(mFlingHitPos, deltaX, deltaY);
+ } else if (Math.abs(y2 - y1) > mTouchSlop) {
+ mCanDrag = false; // if started to scroll the list then
+ // don't allow sorting nor fling-removing
+ }
+ }
+ }
+ // return whatever
+ return false;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ // Log.d("mobeta", "lift listener long pressed");
+ if (mHitPos != MISS && mDragInitMode == ON_LONG_PRESS) {
+ mDslv.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ startDrag(mHitPos, mCurrX - mItemX, mCurrY - mItemY);
+ }
+ }
+
+ // complete the OnGestureListener interface
+ @Override
+ public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return false;
+ }
+
+ // complete the OnGestureListener interface
+ @Override
+ public boolean onSingleTapUp(MotionEvent ev) {
+ if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
+ if (mClickRemoveHitPos != MISS) {
+ mDslv.removeItem(mClickRemoveHitPos - mDslv.getHeaderViewsCount());
+ }
+ }
+ return true;
+ }
+
+ // complete the OnGestureListener interface
+ @Override
+ public void onShowPress(MotionEvent ev) {
+ // do nothing
+ }
+
+ private GestureDetector.OnGestureListener mFlingRemoveListener =
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ // Log.d("mobeta", "on fling remove called");
+ if (mRemoveEnabled && mIsRemoving) {
+ int w = mDslv.getWidth();
+ int minPos = w / 5;
+ if (velocityX > mFlingSpeed) {
+ if (mPositionX > -minPos) {
+ mDslv.stopDragWithVelocity(true, velocityX);
+ }
+ } else if (velocityX < -mFlingSpeed) {
+ if (mPositionX < minPos) {
+ mDslv.stopDragWithVelocity(true, velocityX);
+ }
+ }
+ mIsRemoving = false;
+ }
+ return false;
+ }
+ };
+
+}
diff --git a/ring-android/src/cx/ring/views/dragsortlv/DragSortCursorAdapter.java b/ring-android/src/cx/ring/views/dragsortlv/DragSortCursorAdapter.java
new file mode 100644
index 0000000..1700d44
--- /dev/null
+++ b/ring-android/src/cx/ring/views/dragsortlv/DragSortCursorAdapter.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2004-2013 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ *
+ * Part of the library:
+ * https://github.com/bauerca/drag-sort-listview
+ * No longer maintained
+ *
+ *
+ */
+
+package cx.ring.views.dragsortlv;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.support.v4.widget.CursorAdapter;
+
+
+/**
+ * A subclass of {@link android.widget.CursorAdapter} that provides
+ * reordering of the elements in the Cursor based on completed
+ * drag-sort operations. The reordering is a simple mapping of
+ * list positions into Cursor positions (the Cursor is unchanged).
+ * To persist changes made by drag-sorts, one can retrieve the
+ * mapping with the {@link #getCursorPositions()} method, which
+ * returns the reordered list of Cursor positions.
+ *
+ * An instance of this class is passed
+ * to {@link DragSortListView#setAdapter(ListAdapter)} and, since
+ * this class implements the {@link DragSortListView.DragSortListener}
+ * interface, it is automatically set as the DragSortListener for
+ * the DragSortListView instance.
+ */
+public abstract class DragSortCursorAdapter extends CursorAdapter implements DragSortListView.DragSortListener {
+
+ public static final int REMOVED = -1;
+
+ /**
+ * Key is ListView position, value is Cursor position
+ */
+ private SparseIntArray mListMapping = new SparseIntArray();
+
+ private ArrayList<Integer> mRemovedCursorPositions = new ArrayList<Integer>();
+
+ public DragSortCursorAdapter(Context context, Cursor c) {
+ super(context, c);
+ }
+
+ public DragSortCursorAdapter(Context context, Cursor c, boolean autoRequery) {
+ super(context, c, autoRequery);
+ }
+
+ public DragSortCursorAdapter(Context context, Cursor c, int flags) {
+ super(context, c, flags);
+ }
+
+ /**
+ * Swaps Cursor and clears list-Cursor mapping.
+ *
+ * @see android.widget.CursorAdapter#swapCursor(android.database.Cursor)
+ */
+ @Override
+ public Cursor swapCursor(Cursor newCursor) {
+ Cursor old = super.swapCursor(newCursor);
+ resetMappings();
+ return old;
+ }
+
+ /**
+ * Changes Cursor and clears list-Cursor mapping.
+ *
+ * @see android.widget.CursorAdapter#changeCursor(android.database.Cursor)
+ */
+ @Override
+ public void changeCursor(Cursor cursor) {
+ super.changeCursor(cursor);
+ resetMappings();
+ }
+
+ /**
+ * Resets list-cursor mapping.
+ */
+ public void reset() {
+ resetMappings();
+ notifyDataSetChanged();
+ }
+
+ private void resetMappings() {
+ mListMapping.clear();
+ mRemovedCursorPositions.clear();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return super.getItem(mListMapping.get(position, position));
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return super.getItemId(mListMapping.get(position, position));
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ return super.getDropDownView(mListMapping.get(position, position), convertView, parent);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return super.getView(mListMapping.get(position, position), convertView, parent);
+ }
+
+ /**
+ * On drop, this updates the mapping between Cursor positions
+ * and ListView positions. The Cursor is unchanged. Retrieve
+ * the current mapping with {@link getCursorPositions()}.
+ *
+ * @see DragSortListView.DropListener#drop(int, int)
+ */
+ @Override
+ public void drop(int from, int to) {
+ if (from != to) {
+ int cursorFrom = mListMapping.get(from, from);
+
+ if (from > to) {
+ for (int i = from; i > to; --i) {
+ mListMapping.put(i, mListMapping.get(i - 1, i - 1));
+ }
+ } else {
+ for (int i = from; i < to; ++i) {
+ mListMapping.put(i, mListMapping.get(i + 1, i + 1));
+ }
+ }
+ mListMapping.put(to, cursorFrom);
+
+ cleanMapping();
+ notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * On remove, this updates the mapping between Cursor positions
+ * and ListView positions. The Cursor is unchanged. Retrieve
+ * the current mapping with {@link getCursorPositions()}.
+ *
+ * @see DragSortListView.RemoveListener#remove(int)
+ */
+ @Override
+ public void remove(int which) {
+ int cursorPos = mListMapping.get(which, which);
+ if (!mRemovedCursorPositions.contains(cursorPos)) {
+ mRemovedCursorPositions.add(cursorPos);
+ }
+
+ int newCount = getCount();
+ for (int i = which; i < newCount; ++i) {
+ mListMapping.put(i, mListMapping.get(i + 1, i + 1));
+ }
+
+ mListMapping.delete(newCount);
+
+ cleanMapping();
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Does nothing. Just completes DragSortListener interface.
+ */
+ @Override
+ public void drag(int from, int to) {
+ // do nothing
+ }
+
+ /**
+ * Remove unnecessary mappings from sparse array.
+ */
+ private void cleanMapping() {
+ ArrayList<Integer> toRemove = new ArrayList<Integer>();
+
+ int size = mListMapping.size();
+ for (int i = 0; i < size; ++i) {
+ if (mListMapping.keyAt(i) == mListMapping.valueAt(i)) {
+ toRemove.add(mListMapping.keyAt(i));
+ }
+ }
+
+ size = toRemove.size();
+ for (int i = 0; i < size; ++i) {
+ mListMapping.delete(toRemove.get(i));
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return super.getCount() - mRemovedCursorPositions.size();
+ }
+
+ /**
+ * Get the Cursor position mapped to by the provided list position
+ * (given all previously handled drag-sort
+ * operations).
+ *
+ * @param position List position
+ *
+ * @return The mapped-to Cursor position
+ */
+ public int getCursorPosition(int position) {
+ return mListMapping.get(position, position);
+ }
+
+ /**
+ * Get the current order of Cursor positions presented by the
+ * list.
+ */
+ public ArrayList<Integer> getCursorPositions() {
+ ArrayList<Integer> result = new ArrayList<Integer>();
+
+ for (int i = 0; i < getCount(); ++i) {
+ result.add(mListMapping.get(i, i));
+ }
+
+ return result;
+ }
+
+ /**
+ * Get the list position mapped to by the provided Cursor position.
+ * If the provided Cursor position has been removed by a drag-sort,
+ * this returns {@link #REMOVED}.
+ *
+ * @param cursorPosition A Cursor position
+ * @return The mapped-to list position or REMOVED
+ */
+ public int getListPosition(int cursorPosition) {
+ if (mRemovedCursorPositions.contains(cursorPosition)) {
+ return REMOVED;
+ }
+
+ int index = mListMapping.indexOfValue(cursorPosition);
+ if (index < 0) {
+ return cursorPosition;
+ } else {
+ return mListMapping.keyAt(index);
+ }
+ }
+
+
+}
diff --git a/ring-android/src/cx/ring/views/dragsortlv/DragSortItemView.java b/ring-android/src/cx/ring/views/dragsortlv/DragSortItemView.java
new file mode 100644
index 0000000..0bb976c
--- /dev/null
+++ b/ring-android/src/cx/ring/views/dragsortlv/DragSortItemView.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2004-2013 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ *
+ * Part of the library:
+ * https://github.com/bauerca/drag-sort-listview
+ * No longer maintained
+ *
+ *
+ */
+
+package cx.ring.views.dragsortlv;
+
+import android.content.Context;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+
+/**
+ * Lightweight ViewGroup that wraps list items obtained from user's
+ * ListAdapter. ItemView expects a single child that has a definite
+ * height (i.e. the child's layout height is not MATCH_PARENT).
+ * The width of
+ * ItemView will always match the width of its child (that is,
+ * the width MeasureSpec given to ItemView is passed directly
+ * to the child, and the ItemView measured width is set to the
+ * child's measured width). The height of ItemView can be anything;
+ * the
+ *
+ *
+ * The purpose of this class is to optimize slide
+ * shuffle animations.
+ */
+public class DragSortItemView extends ViewGroup {
+
+ private int mGravity = Gravity.TOP;
+
+ public DragSortItemView(Context context) {
+ super(context);
+
+ // always init with standard ListView layout params
+ setLayoutParams(new AbsListView.LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ //setClipChildren(true);
+ }
+
+ public void setGravity(int gravity) {
+ mGravity = gravity;
+ }
+
+ public int getGravity() {
+ return mGravity;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final View child = getChildAt(0);
+
+ if (child == null) {
+ return;
+ }
+
+ if (mGravity == Gravity.TOP) {
+ child.layout(0, 0, getMeasuredWidth(), child.getMeasuredHeight());
+ } else {
+ child.layout(0, getMeasuredHeight() - child.getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight());
+ }
+ }
+
+ /**
+ *
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ final View child = getChildAt(0);
+ if (child == null) {
+ setMeasuredDimension(0, width);
+ return;
+ }
+
+ if (child.isLayoutRequested()) {
+ // Always let child be as tall as it wants.
+ measureChild(child, widthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ }
+
+ if (heightMode == MeasureSpec.UNSPECIFIED) {
+ ViewGroup.LayoutParams lp = getLayoutParams();
+
+ if (lp.height > 0) {
+ height = lp.height;
+ } else {
+ height = child.getMeasuredHeight();
+ }
+ }
+
+ setMeasuredDimension(width, height);
+ }
+
+}
diff --git a/ring-android/src/cx/ring/views/dragsortlv/DragSortItemViewCheckable.java b/ring-android/src/cx/ring/views/dragsortlv/DragSortItemViewCheckable.java
new file mode 100644
index 0000000..c4a176f
--- /dev/null
+++ b/ring-android/src/cx/ring/views/dragsortlv/DragSortItemViewCheckable.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2004-2013 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ *
+ * Part of the library:
+ * https://github.com/bauerca/drag-sort-listview
+ * No longer maintained
+ *
+ *
+ */
+
+package cx.ring.views.dragsortlv;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.Checkable;
+
+/**
+ * Lightweight ViewGroup that wraps list items obtained from user's
+ * ListAdapter. ItemView expects a single child that has a definite
+ * height (i.e. the child's layout height is not MATCH_PARENT).
+ * The width of
+ * ItemView will always match the width of its child (that is,
+ * the width MeasureSpec given to ItemView is passed directly
+ * to the child, and the ItemView measured width is set to the
+ * child's measured width). The height of ItemView can be anything;
+ * the
+ *
+ *
+ * The purpose of this class is to optimize slide
+ * shuffle animations.
+ */
+public class DragSortItemViewCheckable extends DragSortItemView implements Checkable {
+
+ public DragSortItemViewCheckable(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean isChecked() {
+ View child = getChildAt(0);
+ if (child instanceof Checkable)
+ return ((Checkable) child).isChecked();
+ else
+ return false;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ View child = getChildAt(0);
+ if (child instanceof Checkable)
+ ((Checkable) child).setChecked(checked);
+ }
+
+ @Override
+ public void toggle() {
+ View child = getChildAt(0);
+ if (child instanceof Checkable)
+ ((Checkable) child).toggle();
+ }
+}
diff --git a/ring-android/src/cx/ring/views/dragsortlv/DragSortListView.java b/ring-android/src/cx/ring/views/dragsortlv/DragSortListView.java
new file mode 100644
index 0000000..3655fb2
--- /dev/null
+++ b/ring-android/src/cx/ring/views/dragsortlv/DragSortListView.java
@@ -0,0 +1,3074 @@
+/*
+ * DragSortListView.
+ *
+ * A subclass of the Android ListView component that enables drag
+ * and drop re-ordering of list items.
+ *
+ * Copyright 2012 Carl Bauer
+ *
+ * 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 cx.ring.views.dragsortlv;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.BaseAdapter;
+import android.widget.Checkable;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+
+import cx.ring.R;
+
+/**
+ * ListView subclass that mediates drag and drop resorting of items.
+ *
+ *
+ * @author heycosmo
+ *
+ */
+public class DragSortListView extends ListView {
+
+
+ /**
+ * The View that floats above the ListView and represents
+ * the dragged item.
+ */
+ private View mFloatView;
+
+ /**
+ * The float View location. First based on touch location
+ * and given deltaX and deltaY. Then restricted by callback
+ * to FloatViewManager.onDragFloatView(). Finally restricted
+ * by bounds of DSLV.
+ */
+ private Point mFloatLoc = new Point();
+
+ private Point mTouchLoc = new Point();
+
+ /**
+ * The middle (in the y-direction) of the floating View.
+ */
+ private int mFloatViewMid;
+
+ /**
+ * Flag to make sure float View isn't measured twice
+ */
+ private boolean mFloatViewOnMeasured = false;
+
+ /**
+ * Watch the Adapter for data changes. Cancel a drag if
+ * coincident with a change.
+ */
+ private DataSetObserver mObserver;
+
+ /**
+ * Transparency for the floating View (XML attribute).
+ */
+ private float mFloatAlpha = 1.0f;
+ private float mCurrFloatAlpha = 1.0f;
+
+ /**
+ * While drag-sorting, the current position of the floating
+ * View. If dropped, the dragged item will land in this position.
+ */
+ private int mFloatPos;
+
+ /**
+ * The first expanded ListView position that helps represent
+ * the drop slot tracking the floating View.
+ */
+ private int mFirstExpPos;
+
+ /**
+ * The second expanded ListView position that helps represent
+ * the drop slot tracking the floating View. This can equal
+ * mFirstExpPos if there is no slide shuffle occurring; otherwise
+ * it is equal to mFirstExpPos + 1.
+ */
+ private int mSecondExpPos;
+
+ /**
+ * Flag set if slide shuffling is enabled.
+ */
+ private boolean mAnimate = false;
+
+ /**
+ * The user dragged from this position.
+ */
+ private int mSrcPos;
+
+ /**
+ * Offset (in x) within the dragged item at which the user
+ * picked it up (or first touched down with the digitalis).
+ */
+ private int mDragDeltaX;
+
+ /**
+ * Offset (in y) within the dragged item at which the user
+ * picked it up (or first touched down with the digitalis).
+ */
+ private int mDragDeltaY;
+
+
+ /**
+ * The difference (in x) between screen coordinates and coordinates
+ * in this view.
+ */
+ private int mOffsetX;
+
+ /**
+ * The difference (in y) between screen coordinates and coordinates
+ * in this view.
+ */
+ private int mOffsetY;
+
+ /**
+ * A listener that receives callbacks whenever the floating View
+ * hovers over a new position.
+ */
+ private DragListener mDragListener;
+
+ /**
+ * A listener that receives a callback when the floating View
+ * is dropped.
+ */
+ private DropListener mDropListener;
+
+ /**
+ * A listener that receives a callback when the floating View
+ * (or more precisely the originally dragged item) is removed
+ * by one of the provided gestures.
+ */
+ private RemoveListener mRemoveListener;
+
+ /**
+ * Enable/Disable item dragging
+ *
+ * @attr name dslv:drag_enabled
+ */
+ private boolean mDragEnabled = true;
+
+ /**
+ * Drag state enum.
+ */
+ private final static int IDLE = 0;
+ private final static int REMOVING = 1;
+ private final static int DROPPING = 2;
+ private final static int STOPPED = 3;
+ private final static int DRAGGING = 4;
+
+ private int mDragState = IDLE;
+
+ /**
+ * Height in pixels to which the originally dragged item
+ * is collapsed during a drag-sort. Currently, this value
+ * must be greater than zero.
+ */
+ private int mItemHeightCollapsed = 1;
+
+ /**
+ * Height of the floating View. Stored for the purpose of
+ * providing the tracking drop slot.
+ */
+ private int mFloatViewHeight;
+
+ /**
+ * Convenience member. See above.
+ */
+ private int mFloatViewHeightHalf;
+
+ /**
+ * Save the given width spec for use in measuring children
+ */
+ private int mWidthMeasureSpec = 0;
+
+ /**
+ * Sample Views ultimately used for calculating the height
+ * of ListView items that are off-screen.
+ */
+ private View[] mSampleViewTypes = new View[1];
+
+ /**
+ * Drag-scroll encapsulator!
+ */
+ private DragScroller mDragScroller;
+
+ /**
+ * Determines the start of the upward drag-scroll region
+ * at the top of the ListView. Specified by a fraction
+ * of the ListView height, thus screen resolution agnostic.
+ */
+ private float mDragUpScrollStartFrac = 1.0f / 3.0f;
+
+ /**
+ * Determines the start of the downward drag-scroll region
+ * at the bottom of the ListView. Specified by a fraction
+ * of the ListView height, thus screen resolution agnostic.
+ */
+ private float mDragDownScrollStartFrac = 1.0f / 3.0f;
+
+ /**
+ * The following are calculated from the above fracs.
+ */
+ private int mUpScrollStartY;
+ private int mDownScrollStartY;
+ private float mDownScrollStartYF;
+ private float mUpScrollStartYF;
+
+ /**
+ * Calculated from above above and current ListView height.
+ */
+ private float mDragUpScrollHeight;
+
+ /**
+ * Calculated from above above and current ListView height.
+ */
+ private float mDragDownScrollHeight;
+
+ /**
+ * Maximum drag-scroll speed in pixels per ms. Only used with
+ * default linear drag-scroll profile.
+ */
+ private float mMaxScrollSpeed = 0.5f;
+
+ /**
+ * Defines the scroll speed during a drag-scroll. User can
+ * provide their own; this default is a simple linear profile
+ * where scroll speed increases linearly as the floating View
+ * nears the top/bottom of the ListView.
+ */
+ private DragScrollProfile mScrollProfile = new DragScrollProfile() {
+ @Override
+ public float getSpeed(float w, long t) {
+ return mMaxScrollSpeed * w;
+ }
+ };
+
+ /**
+ * Current touch x.
+ */
+ private int mX;
+
+ /**
+ * Current touch y.
+ */
+ private int mY;
+
+ /**
+ * Last touch x.
+ */
+ private int mLastX;
+
+ /**
+ * Last touch y.
+ */
+ private int mLastY;
+
+ /**
+ * The touch y-coord at which drag started
+ */
+ private int mDragStartY;
+
+ /**
+ * Drag flag bit. Floating View can move in the positive
+ * x direction.
+ */
+ public final static int DRAG_POS_X = 0x1;
+
+ /**
+ * Drag flag bit. Floating View can move in the negative
+ * x direction.
+ */
+ public final static int DRAG_NEG_X = 0x2;
+
+ /**
+ * Drag flag bit. Floating View can move in the positive
+ * y direction. This is subtle. What this actually means is
+ * that, if enabled, the floating View can be dragged below its starting
+ * position. Remove in favor of upper-bounding item position?
+ */
+ public final static int DRAG_POS_Y = 0x4;
+
+ /**
+ * Drag flag bit. Floating View can move in the negative
+ * y direction. This is subtle. What this actually means is
+ * that the floating View can be dragged above its starting
+ * position. Remove in favor of lower-bounding item position?
+ */
+ public final static int DRAG_NEG_Y = 0x8;
+
+ /**
+ * Flags that determine limits on the motion of the
+ * floating View. See flags above.
+ */
+ private int mDragFlags = 0;
+
+ /**
+ * Last call to an on*TouchEvent was a call to
+ * onInterceptTouchEvent.
+ */
+ private boolean mLastCallWasIntercept = false;
+
+ /**
+ * A touch event is in progress.
+ */
+ private boolean mInTouchEvent = false;
+
+ /**
+ * Let the user customize the floating View.
+ */
+ private FloatViewManager mFloatViewManager = null;
+
+ /**
+ * Given to ListView to cancel its action when a drag-sort
+ * begins.
+ */
+ private MotionEvent mCancelEvent;
+
+ /**
+ * Enum telling where to cancel the ListView action when a
+ * drag-sort begins
+ */
+ private static final int NO_CANCEL = 0;
+ private static final int ON_TOUCH_EVENT = 1;
+ private static final int ON_INTERCEPT_TOUCH_EVENT = 2;
+
+ /**
+ * Where to cancel the ListView action when a
+ * drag-sort begins
+ */
+ private int mCancelMethod = NO_CANCEL;
+
+ /**
+ * Determines when a slide shuffle animation starts. That is,
+ * defines how close to the edge of the drop slot the floating
+ * View must be to initiate the slide.
+ */
+ private float mSlideRegionFrac = 0.25f;
+
+ /**
+ * Number between 0 and 1 indicating the relative location of
+ * a sliding item (only used if drag-sort animations
+ * are turned on). Nearly 1 means the item is
+ * at the top of the slide region (nearly full blank item
+ * is directly below).
+ */
+ private float mSlideFrac = 0.0f;
+
+ /**
+ * Wraps the user-provided ListAdapter. This is used to wrap each
+ * item View given by the user inside another View (currenly
+ * a RelativeLayout) which
+ * expands and collapses to simulate the item shuffling.
+ */
+ private AdapterWrapper mAdapterWrapper;
+
+ /**
+ * Turn on custom debugger.
+ */
+ private boolean mTrackDragSort = false;
+
+ /**
+ * Debugging class.
+ */
+ private DragSortTracker mDragSortTracker;
+
+ /**
+ * Needed for adjusting item heights from within layoutChildren
+ */
+ private boolean mBlockLayoutRequests = false;
+
+ /**
+ * Set to true when a down event happens during drag sort;
+ * for example, when drag finish animations are
+ * playing.
+ */
+ private boolean mIgnoreTouchEvent = false;
+
+ /**
+ * Caches DragSortItemView child heights. Sometimes DSLV has to
+ * know the height of an offscreen item. Since ListView virtualizes
+ * these, DSLV must get the item from the ListAdapter to obtain
+ * its height. That process can be expensive, but often the same
+ * offscreen item will be requested many times in a row. Once an
+ * offscreen item height is calculated, we cache it in this guy.
+ * Actually, we cache the height of the child of the
+ * DragSortItemView since the item height changes often during a
+ * drag-sort.
+ */
+ private static final int sCacheSize = 3;
+ private HeightCache mChildHeightCache = new HeightCache(sCacheSize);
+
+ private RemoveAnimator mRemoveAnimator;
+
+ private LiftAnimator mLiftAnimator;
+
+ private DropAnimator mDropAnimator;
+
+ private boolean mUseRemoveVelocity;
+ private float mRemoveVelocityX = 0;
+
+ public DragSortListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ int defaultDuration = 150;
+ int removeAnimDuration = defaultDuration; // ms
+ int dropAnimDuration = defaultDuration; // ms
+
+ if (attrs != null) {
+ TypedArray a = getContext().obtainStyledAttributes(attrs,
+ R.styleable.DragSortListView, 0, 0);
+
+ mItemHeightCollapsed = Math.max(1, a.getDimensionPixelSize(
+ R.styleable.DragSortListView_collapsed_height, 1));
+
+ mTrackDragSort = a.getBoolean(
+ R.styleable.DragSortListView_track_drag_sort, false);
+
+ if (mTrackDragSort) {
+ mDragSortTracker = new DragSortTracker();
+ }
+
+ // alpha between 0 and 255, 0=transparent, 255=opaque
+ mFloatAlpha = a.getFloat(R.styleable.DragSortListView_float_alpha, mFloatAlpha);
+ mCurrFloatAlpha = mFloatAlpha;
+
+ mDragEnabled = a.getBoolean(R.styleable.DragSortListView_drag_enabled, mDragEnabled);
+
+ mSlideRegionFrac = Math.max(0.0f,
+ Math.min(1.0f, 1.0f - a.getFloat(
+ R.styleable.DragSortListView_slide_shuffle_speed,
+ 0.75f)));
+
+ mAnimate = mSlideRegionFrac > 0.0f;
+
+ float frac = a.getFloat(
+ R.styleable.DragSortListView_drag_scroll_start,
+ mDragUpScrollStartFrac);
+
+ setDragScrollStart(frac);
+
+ mMaxScrollSpeed = a.getFloat(
+ R.styleable.DragSortListView_max_drag_scroll_speed,
+ mMaxScrollSpeed);
+
+ removeAnimDuration = a.getInt(
+ R.styleable.DragSortListView_remove_animation_duration,
+ removeAnimDuration);
+
+ dropAnimDuration = a.getInt(
+ R.styleable.DragSortListView_drop_animation_duration,
+ dropAnimDuration);
+
+ boolean useDefault = a.getBoolean(
+ R.styleable.DragSortListView_use_default_controller,
+ true);
+
+ if (useDefault) {
+ boolean removeEnabled = a.getBoolean(
+ R.styleable.DragSortListView_remove_enabled,
+ false);
+ int removeMode = a.getInt(
+ R.styleable.DragSortListView_remove_mode,
+ DragSortController.FLING_REMOVE);
+ boolean sortEnabled = a.getBoolean(
+ R.styleable.DragSortListView_sort_enabled,
+ true);
+ int dragInitMode = a.getInt(
+ R.styleable.DragSortListView_drag_start_mode,
+ DragSortController.ON_DOWN);
+ int dragHandleId = a.getResourceId(
+ R.styleable.DragSortListView_drag_handle_id,
+ 0);
+ int flingHandleId = a.getResourceId(
+ R.styleable.DragSortListView_fling_handle_id,
+ 0);
+ int clickRemoveId = a.getResourceId(
+ R.styleable.DragSortListView_click_remove_id,
+ 0);
+ int bgColor = a.getColor(
+ R.styleable.DragSortListView_float_background_color,
+ Color.BLACK);
+
+ DragSortController controller = new DragSortController(
+ this, dragHandleId, dragInitMode, removeMode,
+ clickRemoveId, flingHandleId);
+ controller.setRemoveEnabled(removeEnabled);
+ controller.setSortEnabled(sortEnabled);
+ controller.setBackgroundColor(bgColor);
+
+ mFloatViewManager = controller;
+ setOnTouchListener(controller);
+ }
+
+ a.recycle();
+ }
+
+ mDragScroller = new DragScroller();
+
+ float smoothness = 0.5f;
+ if (removeAnimDuration > 0) {
+ mRemoveAnimator = new RemoveAnimator(smoothness, removeAnimDuration);
+ }
+ // mLiftAnimator = new LiftAnimator(smoothness, 100);
+ if (dropAnimDuration > 0) {
+ mDropAnimator = new DropAnimator(smoothness, dropAnimDuration);
+ }
+
+ mCancelEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0f, 0f, 0, 0f,
+ 0f, 0, 0);
+
+ // construct the dataset observer
+ mObserver = new DataSetObserver() {
+ private void cancel() {
+ if (mDragState == DRAGGING) {
+ cancelDrag();
+ }
+ }
+
+ @Override
+ public void onChanged() {
+ cancel();
+ }
+
+ @Override
+ public void onInvalidated() {
+ cancel();
+ }
+ };
+ }
+
+ /**
+ * Usually called from a FloatViewManager. The float alpha
+ * will be reset to the xml-defined value every time a drag
+ * is stopped.
+ */
+ public void setFloatAlpha(float alpha) {
+ mCurrFloatAlpha = alpha;
+ }
+
+ public float getFloatAlpha() {
+ return mCurrFloatAlpha;
+ }
+
+ /**
+ * Set maximum drag scroll speed in positions/second. Only applies
+ * if using default ScrollSpeedProfile.
+ *
+ * @param max Maximum scroll speed.
+ */
+ public void setMaxScrollSpeed(float max) {
+ mMaxScrollSpeed = max;
+ }
+
+ /**
+ * For each DragSortListView Listener interface implemented by
+ * <code>adapter</code>, this method calls the appropriate
+ * set*Listener method with <code>adapter</code> as the argument.
+ *
+ * @param adapter The ListAdapter providing data to back
+ * DragSortListView.
+ *
+ * @see android.widget.ListView#setAdapter(android.widget.ListAdapter)
+ */
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ if (adapter != null) {
+ mAdapterWrapper = new AdapterWrapper(adapter);
+ adapter.registerDataSetObserver(mObserver);
+
+ if (adapter instanceof DropListener) {
+ setDropListener((DropListener) adapter);
+ }
+ if (adapter instanceof DragListener) {
+ setDragListener((DragListener) adapter);
+ }
+ if (adapter instanceof RemoveListener) {
+ setRemoveListener((RemoveListener) adapter);
+ }
+ } else {
+ mAdapterWrapper = null;
+ }
+
+ super.setAdapter(mAdapterWrapper);
+ }
+
+ /**
+ * As opposed to {@link ListView#getAdapter()}, which returns
+ * a heavily wrapped ListAdapter (DragSortListView wraps the
+ * input ListAdapter {\emph and} ListView wraps the wrapped one).
+ *
+ * @return The ListAdapter set as the argument of {@link setAdapter()}
+ */
+ public ListAdapter getInputAdapter() {
+ if (mAdapterWrapper == null) {
+ return null;
+ } else {
+ return mAdapterWrapper.getAdapter();
+ }
+ }
+
+ private class AdapterWrapper extends BaseAdapter {
+ private ListAdapter mAdapter;
+
+ public AdapterWrapper(ListAdapter adapter) {
+ super();
+ mAdapter = adapter;
+
+ mAdapter.registerDataSetObserver(new DataSetObserver() {
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ public void onInvalidated() {
+ notifyDataSetInvalidated();
+ }
+ });
+ }
+
+ public ListAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mAdapter.getItemId(position);
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mAdapter.getItem(position);
+ }
+
+ @Override
+ public int getCount() {
+ return mAdapter.getCount();
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return mAdapter.areAllItemsEnabled();
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return mAdapter.isEnabled(position);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return mAdapter.getItemViewType(position);
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return mAdapter.getViewTypeCount();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return mAdapter.hasStableIds();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return mAdapter.isEmpty();
+ }
+
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+
+ DragSortItemView v;
+ View child;
+ // Log.d("mobeta",
+ // "getView: position="+position+" convertView="+convertView);
+ if (convertView != null) {
+ v = (DragSortItemView) convertView;
+ View oldChild = v.getChildAt(0);
+
+ child = mAdapter.getView(position, oldChild, DragSortListView.this);
+ if (child != oldChild) {
+ // shouldn't get here if user is reusing convertViews
+ // properly
+ if (oldChild != null) {
+ v.removeViewAt(0);
+ }
+ v.addView(child);
+ }
+ } else {
+ child = mAdapter.getView(position, null, DragSortListView.this);
+ if (child instanceof Checkable) {
+ v = new DragSortItemViewCheckable(getContext());
+ } else {
+ v = new DragSortItemView(getContext());
+ }
+ v.setLayoutParams(new AbsListView.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ v.addView(child);
+ }
+
+ // Set the correct item height given drag state; passed
+ // View needs to be measured if measurement is required.
+ adjustItem(position + getHeaderViewsCount(), v, true);
+
+ return v;
+ }
+ }
+
+ private void drawDivider(int expPosition, Canvas canvas) {
+
+ final Drawable divider = getDivider();
+ final int dividerHeight = getDividerHeight();
+ // Log.d("mobeta", "div="+divider+" divH="+dividerHeight);
+
+ if (divider != null && dividerHeight != 0) {
+ final ViewGroup expItem = (ViewGroup) getChildAt(expPosition
+ - getFirstVisiblePosition());
+ if (expItem != null) {
+ final int l = getPaddingLeft();
+ final int r = getWidth() - getPaddingRight();
+ final int t;
+ final int b;
+
+ final int childHeight = expItem.getChildAt(0).getHeight();
+
+ if (expPosition > mSrcPos) {
+ t = expItem.getTop() + childHeight;
+ b = t + dividerHeight;
+ } else {
+ b = expItem.getBottom() - childHeight;
+ t = b - dividerHeight;
+ }
+ // Log.d("mobeta", "l="+l+" t="+t+" r="+r+" b="+b);
+
+ // Have to clip to support ColorDrawable on <= Gingerbread
+ canvas.save();
+ canvas.clipRect(l, t, r, b);
+ divider.setBounds(l, t, r, b);
+ divider.draw(canvas);
+ canvas.restore();
+ }
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ if (mDragState != IDLE) {
+ // draw the divider over the expanded item
+ if (mFirstExpPos != mSrcPos) {
+ drawDivider(mFirstExpPos, canvas);
+ }
+ if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) {
+ drawDivider(mSecondExpPos, canvas);
+ }
+ }
+
+ if (mFloatView != null) {
+ // draw the float view over everything
+ final int w = mFloatView.getWidth();
+ final int h = mFloatView.getHeight();
+
+ int x = mFloatLoc.x;
+
+ int width = getWidth();
+ if (x < 0)
+ x = -x;
+ float alphaMod;
+ if (x < width) {
+ alphaMod = ((float) (width - x)) / ((float) width);
+ alphaMod *= alphaMod;
+ } else {
+ alphaMod = 0;
+ }
+
+ final int alpha = (int) (255f * mCurrFloatAlpha * alphaMod);
+
+ canvas.save();
+ // Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds());
+ canvas.translate(mFloatLoc.x, mFloatLoc.y);
+ canvas.clipRect(0, 0, w, h);
+
+ // Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds());
+ canvas.saveLayerAlpha(0, 0, w, h, alpha, Canvas.ALL_SAVE_FLAG);
+ mFloatView.draw(canvas);
+ canvas.restore();
+ canvas.restore();
+ }
+ }
+
+ private int getItemHeight(int position) {
+ View v = getChildAt(position - getFirstVisiblePosition());
+
+ if (v != null) {
+ // item is onscreen, just get the height of the View
+ return v.getHeight();
+ } else {
+ // item is offscreen. get child height and calculate
+ // item height based on current shuffle state
+ return calcItemHeight(position, getChildHeight(position));
+ }
+ }
+
+ private void printPosData() {
+ Log.d("mobeta", "mSrcPos=" + mSrcPos + " mFirstExpPos=" + mFirstExpPos + " mSecondExpPos="
+ + mSecondExpPos);
+ }
+
+ private class HeightCache {
+
+ private SparseIntArray mMap;
+ private ArrayList<Integer> mOrder;
+ private int mMaxSize;
+
+ public HeightCache(int size) {
+ mMap = new SparseIntArray(size);
+ mOrder = new ArrayList<Integer>(size);
+ mMaxSize = size;
+ }
+
+ /**
+ * Add item height at position if doesn't already exist.
+ */
+ public void add(int position, int height) {
+ int currHeight = mMap.get(position, -1);
+ if (currHeight != height) {
+ if (currHeight == -1) {
+ if (mMap.size() == mMaxSize) {
+ // remove oldest entry
+ mMap.delete(mOrder.remove(0));
+ }
+ } else {
+ // move position to newest slot
+ mOrder.remove((Integer) position);
+ }
+ mMap.put(position, height);
+ mOrder.add(position);
+ }
+ }
+
+ public int get(int position) {
+ return mMap.get(position, -1);
+ }
+
+ public void clear() {
+ mMap.clear();
+ mOrder.clear();
+ }
+
+ }
+
+ /**
+ * Get the shuffle edge for item at position when top of
+ * item is at y-coord top. Assumes that current item heights
+ * are consistent with current float view location and
+ * thus expanded positions and slide fraction. i.e. Should not be
+ * called between update of expanded positions/slide fraction
+ * and layoutChildren.
+ *
+ * @param position
+ * @param top
+ * calculates this height.
+ *
+ * @return Shuffle line between position-1 and position (for
+ * the given view of the list; that is, for when top of item at
+ * position has y-coord of given `top`). If
+ * floating View (treated as horizontal line) is dropped
+ * immediately above this line, it lands in position-1. If
+ * dropped immediately below this line, it lands in position.
+ */
+ private int getShuffleEdge(int position, int top) {
+
+ final int numHeaders = getHeaderViewsCount();
+ final int numFooters = getFooterViewsCount();
+
+ // shuffle edges are defined between items that can be
+ // dragged; there are N-1 of them if there are N draggable
+ // items.
+
+ if (position <= numHeaders || (position >= getCount() - numFooters)) {
+ return top;
+ }
+
+ int divHeight = getDividerHeight();
+
+ int edge;
+
+ int maxBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
+ int childHeight = getChildHeight(position);
+ int itemHeight = getItemHeight(position);
+
+ // first calculate top of item given that floating View is
+ // centered over src position
+ int otop = top;
+ if (mSecondExpPos <= mSrcPos) {
+ // items are expanded on and/or above the source position
+
+ if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) {
+ if (position == mSrcPos) {
+ otop = top + itemHeight - mFloatViewHeight;
+ } else {
+ int blankHeight = itemHeight - childHeight;
+ otop = top + blankHeight - maxBlankHeight;
+ }
+ } else if (position > mSecondExpPos && position <= mSrcPos) {
+ otop = top - maxBlankHeight;
+ }
+
+ } else {
+ // items are expanded on and/or below the source position
+
+ if (position > mSrcPos && position <= mFirstExpPos) {
+ otop = top + maxBlankHeight;
+ } else if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) {
+ int blankHeight = itemHeight - childHeight;
+ otop = top + blankHeight;
+ }
+ }
+
+ // otop is set
+ if (position <= mSrcPos) {
+ edge = otop + (mFloatViewHeight - divHeight - getChildHeight(position - 1)) / 2;
+ } else {
+ edge = otop + (childHeight - divHeight - mFloatViewHeight) / 2;
+ }
+
+ return edge;
+ }
+
+ private boolean updatePositions() {
+
+ final int first = getFirstVisiblePosition();
+ int startPos = mFirstExpPos;
+ View startView = getChildAt(startPos - first);
+
+ if (startView == null) {
+ startPos = first + getChildCount() / 2;
+ startView = getChildAt(startPos - first);
+ }
+ int startTop = startView.getTop();
+
+ int itemHeight = startView.getHeight();
+
+ int edge = getShuffleEdge(startPos, startTop);
+ int lastEdge = edge;
+
+ int divHeight = getDividerHeight();
+
+ // Log.d("mobeta", "float mid="+mFloatViewMid);
+
+ int itemPos = startPos;
+ int itemTop = startTop;
+ if (mFloatViewMid < edge) {
+ // scanning up for float position
+ // Log.d("mobeta", " edge="+edge);
+ while (itemPos >= 0) {
+ itemPos--;
+ itemHeight = getItemHeight(itemPos);
+
+ if (itemPos == 0) {
+ edge = itemTop - divHeight - itemHeight;
+ break;
+ }
+
+ itemTop -= itemHeight + divHeight;
+ edge = getShuffleEdge(itemPos, itemTop);
+ // Log.d("mobeta", " edge="+edge);
+
+ if (mFloatViewMid >= edge) {
+ break;
+ }
+
+ lastEdge = edge;
+ }
+ } else {
+ // scanning down for float position
+ // Log.d("mobeta", " edge="+edge);
+ final int count = getCount();
+ while (itemPos < count) {
+ if (itemPos == count - 1) {
+ edge = itemTop + divHeight + itemHeight;
+ break;
+ }
+
+ itemTop += divHeight + itemHeight;
+ itemHeight = getItemHeight(itemPos + 1);
+ edge = getShuffleEdge(itemPos + 1, itemTop);
+ // Log.d("mobeta", " edge="+edge);
+
+ // test for hit
+ if (mFloatViewMid < edge) {
+ break;
+ }
+
+ lastEdge = edge;
+ itemPos++;
+ }
+ }
+
+ final int numHeaders = getHeaderViewsCount();
+ final int numFooters = getFooterViewsCount();
+
+ boolean updated = false;
+
+ int oldFirstExpPos = mFirstExpPos;
+ int oldSecondExpPos = mSecondExpPos;
+ float oldSlideFrac = mSlideFrac;
+
+ if (mAnimate) {
+ int edgeToEdge = Math.abs(edge - lastEdge);
+
+ int edgeTop, edgeBottom;
+ if (mFloatViewMid < edge) {
+ edgeBottom = edge;
+ edgeTop = lastEdge;
+ } else {
+ edgeTop = edge;
+ edgeBottom = lastEdge;
+ }
+ // Log.d("mobeta", "edgeTop="+edgeTop+" edgeBot="+edgeBottom);
+
+ int slideRgnHeight = (int) (0.5f * mSlideRegionFrac * edgeToEdge);
+ float slideRgnHeightF = (float) slideRgnHeight;
+ int slideEdgeTop = edgeTop + slideRgnHeight;
+ int slideEdgeBottom = edgeBottom - slideRgnHeight;
+
+ // Three regions
+ if (mFloatViewMid < slideEdgeTop) {
+ mFirstExpPos = itemPos - 1;
+ mSecondExpPos = itemPos;
+ mSlideFrac = 0.5f * ((float) (slideEdgeTop - mFloatViewMid)) / slideRgnHeightF;
+ // Log.d("mobeta",
+ // "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac);
+ } else if (mFloatViewMid < slideEdgeBottom) {
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos;
+ } else {
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos + 1;
+ mSlideFrac = 0.5f * (1.0f + ((float) (edgeBottom - mFloatViewMid))
+ / slideRgnHeightF);
+ // Log.d("mobeta",
+ // "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac);
+ }
+
+ } else {
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos;
+ }
+
+ // correct for headers and footers
+ if (mFirstExpPos < numHeaders) {
+ itemPos = numHeaders;
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos;
+ } else if (mSecondExpPos >= getCount() - numFooters) {
+ itemPos = getCount() - numFooters - 1;
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos;
+ }
+
+ if (mFirstExpPos != oldFirstExpPos || mSecondExpPos != oldSecondExpPos
+ || mSlideFrac != oldSlideFrac) {
+ updated = true;
+ }
+
+ if (itemPos != mFloatPos) {
+ if (mDragListener != null) {
+ mDragListener.drag(mFloatPos - numHeaders, itemPos - numHeaders);
+ }
+
+ mFloatPos = itemPos;
+ updated = true;
+ }
+
+ return updated;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (mTrackDragSort) {
+ mDragSortTracker.appendState();
+ }
+ }
+
+ private class SmoothAnimator implements Runnable {
+ protected long mStartTime;
+
+ private float mDurationF;
+
+ private float mAlpha;
+ private float mA, mB, mC, mD;
+
+ private boolean mCanceled;
+
+ public SmoothAnimator(float smoothness, int duration) {
+ mAlpha = smoothness;
+ mDurationF = (float) duration;
+ mA = mD = 1f / (2f * mAlpha * (1f - mAlpha));
+ mB = mAlpha / (2f * (mAlpha - 1f));
+ mC = 1f / (1f - mAlpha);
+ }
+
+ public float transform(float frac) {
+ if (frac < mAlpha) {
+ return mA * frac * frac;
+ } else if (frac < 1f - mAlpha) {
+ return mB + mC * frac;
+ } else {
+ return 1f - mD * (frac - 1f) * (frac - 1f);
+ }
+ }
+
+ public void start() {
+ mStartTime = SystemClock.uptimeMillis();
+ mCanceled = false;
+ onStart();
+ post(this);
+ }
+
+ public void cancel() {
+ mCanceled = true;
+ }
+
+ public void onStart() {
+ // stub
+ }
+
+ public void onUpdate(float frac, float smoothFrac) {
+ // stub
+ }
+
+ public void onStop() {
+ // stub
+ }
+
+ @Override
+ public void run() {
+ if (mCanceled) {
+ return;
+ }
+
+ float fraction = ((float) (SystemClock.uptimeMillis() - mStartTime)) / mDurationF;
+
+ if (fraction >= 1f) {
+ onUpdate(1f, 1f);
+ onStop();
+ } else {
+ onUpdate(fraction, transform(fraction));
+ post(this);
+ }
+ }
+ }
+
+ /**
+ * Centers floating View under touch point.
+ */
+ private class LiftAnimator extends SmoothAnimator {
+
+ private float mInitDragDeltaY;
+ private float mFinalDragDeltaY;
+
+ public LiftAnimator(float smoothness, int duration) {
+ super(smoothness, duration);
+ }
+
+ @Override
+ public void onStart() {
+ mInitDragDeltaY = mDragDeltaY;
+ mFinalDragDeltaY = mFloatViewHeightHalf;
+ }
+
+ @Override
+ public void onUpdate(float frac, float smoothFrac) {
+ if (mDragState != DRAGGING) {
+ cancel();
+ } else {
+ mDragDeltaY = (int) (smoothFrac * mFinalDragDeltaY + (1f - smoothFrac)
+ * mInitDragDeltaY);
+ mFloatLoc.y = mY - mDragDeltaY;
+ doDragFloatView(true);
+ }
+ }
+ }
+
+ /**
+ * Centers floating View over drop slot before destroying.
+ */
+ private class DropAnimator extends SmoothAnimator {
+
+ private int mDropPos;
+ private int srcPos;
+ private float mInitDeltaY;
+ private float mInitDeltaX;
+
+ public DropAnimator(float smoothness, int duration) {
+ super(smoothness, duration);
+ }
+
+ @Override
+ public void onStart() {
+ mDropPos = mFloatPos;
+ srcPos = mSrcPos;
+ mDragState = DROPPING;
+ mInitDeltaY = mFloatLoc.y - getTargetY();
+ mInitDeltaX = mFloatLoc.x - getPaddingLeft();
+ }
+
+ private int getTargetY() {
+ final int first = getFirstVisiblePosition();
+ final int otherAdjust = (mItemHeightCollapsed + getDividerHeight()) / 2;
+ View v = getChildAt(mDropPos - first);
+ int targetY = -1;
+ if (v != null) {
+ if (mDropPos == srcPos) {
+ targetY = v.getTop();
+ } else if (mDropPos < srcPos) {
+ // expanded down
+ targetY = v.getTop() - otherAdjust;
+ } else {
+ // expanded up
+ targetY = v.getBottom() + otherAdjust - mFloatViewHeight;
+ }
+ } else {
+ // drop position is not on screen?? no animation
+ cancel();
+ }
+
+ return targetY;
+ }
+
+ @Override
+ public void onUpdate(float frac, float smoothFrac) {
+ final int targetY = getTargetY();
+ final int targetX = getPaddingLeft();
+ final float deltaY = mFloatLoc.y - targetY;
+ final float deltaX = mFloatLoc.x - targetX;
+ final float f = 1f - smoothFrac;
+ if (f < Math.abs(deltaY / mInitDeltaY) || f < Math.abs(deltaX / mInitDeltaX)) {
+ mFloatLoc.y = targetY + (int) (mInitDeltaY * f);
+ mFloatLoc.x = getPaddingLeft() + (int) (mInitDeltaX * f);
+ doDragFloatView(true);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ dropFloatView();
+ }
+
+ }
+
+ /**
+ * Collapses expanded items.
+ */
+ private class RemoveAnimator extends SmoothAnimator {
+
+ private float mFloatLocX;
+ private float mFirstStartBlank;
+ private float mSecondStartBlank;
+
+ private int mFirstChildHeight = -1;
+ private int mSecondChildHeight = -1;
+
+ private int mFirstPos;
+ private int mSecondPos;
+ private int srcPos;
+
+ public RemoveAnimator(float smoothness, int duration) {
+ super(smoothness, duration);
+ }
+
+ @Override
+ public void onStart() {
+ mFirstChildHeight = -1;
+ mSecondChildHeight = -1;
+ mFirstPos = mFirstExpPos;
+ mSecondPos = mSecondExpPos;
+ srcPos = mSrcPos;
+ mDragState = REMOVING;
+
+ mFloatLocX = mFloatLoc.x;
+ if (mUseRemoveVelocity) {
+ float minVelocity = 2f * getWidth();
+ if (mRemoveVelocityX == 0) {
+ mRemoveVelocityX = (mFloatLocX < 0 ? -1 : 1) * minVelocity;
+ } else {
+ minVelocity *= 2;
+ if (mRemoveVelocityX < 0 && mRemoveVelocityX > -minVelocity)
+ mRemoveVelocityX = -minVelocity;
+ else if (mRemoveVelocityX > 0 && mRemoveVelocityX < minVelocity)
+ mRemoveVelocityX = minVelocity;
+ }
+ } else {
+ destroyFloatView();
+ }
+ }
+
+ @Override
+ public void onUpdate(float frac, float smoothFrac) {
+ float f = 1f - smoothFrac;
+
+ final int firstVis = getFirstVisiblePosition();
+ View item = getChildAt(mFirstPos - firstVis);
+ ViewGroup.LayoutParams lp;
+ int blank;
+
+ if (mUseRemoveVelocity) {
+ float dt = (float) (SystemClock.uptimeMillis() - mStartTime) / 1000;
+ if (dt == 0)
+ return;
+ float dx = mRemoveVelocityX * dt;
+ int w = getWidth();
+ mRemoveVelocityX += (mRemoveVelocityX > 0 ? 1 : -1) * dt * w;
+ mFloatLocX += dx;
+ mFloatLoc.x = (int) mFloatLocX;
+ if (mFloatLocX < w && mFloatLocX > -w) {
+ mStartTime = SystemClock.uptimeMillis();
+ doDragFloatView(true);
+ return;
+ }
+ }
+
+ if (item != null) {
+ if (mFirstChildHeight == -1) {
+ mFirstChildHeight = getChildHeight(mFirstPos, item, false);
+ mFirstStartBlank = (float) (item.getHeight() - mFirstChildHeight);
+ }
+ blank = Math.max((int) (f * mFirstStartBlank), 1);
+ lp = item.getLayoutParams();
+ lp.height = mFirstChildHeight + blank;
+ item.setLayoutParams(lp);
+ }
+ if (mSecondPos != mFirstPos) {
+ item = getChildAt(mSecondPos - firstVis);
+ if (item != null) {
+ if (mSecondChildHeight == -1) {
+ mSecondChildHeight = getChildHeight(mSecondPos, item, false);
+ mSecondStartBlank = (float) (item.getHeight() - mSecondChildHeight);
+ }
+ blank = Math.max((int) (f * mSecondStartBlank), 1);
+ lp = item.getLayoutParams();
+ lp.height = mSecondChildHeight + blank;
+ item.setLayoutParams(lp);
+ }
+ }
+ }
+
+ @Override
+ public void onStop() {
+ doRemoveItem();
+ }
+ }
+
+ public void removeItem(int which) {
+
+ mUseRemoveVelocity = false;
+ removeItem(which, 0);
+ }
+
+ /**
+ * Removes an item from the list and animates the removal.
+ *
+ * @param which Position to remove (NOTE: headers/footers ignored!
+ * this is a position in your input ListAdapter).
+ * @param velocityX
+ */
+ public void removeItem(int which, float velocityX) {
+ if (mDragState == IDLE || mDragState == DRAGGING) {
+
+ if (mDragState == IDLE) {
+ // called from outside drag-sort
+ mSrcPos = getHeaderViewsCount() + which;
+ mFirstExpPos = mSrcPos;
+ mSecondExpPos = mSrcPos;
+ mFloatPos = mSrcPos;
+ View v = getChildAt(mSrcPos - getFirstVisiblePosition());
+ if (v != null) {
+ v.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ mDragState = REMOVING;
+ mRemoveVelocityX = velocityX;
+
+ if (mInTouchEvent) {
+ switch (mCancelMethod) {
+ case ON_TOUCH_EVENT:
+ super.onTouchEvent(mCancelEvent);
+ break;
+ case ON_INTERCEPT_TOUCH_EVENT:
+ super.onInterceptTouchEvent(mCancelEvent);
+ break;
+ }
+ }
+
+ if (mRemoveAnimator != null) {
+ mRemoveAnimator.start();
+ } else {
+ doRemoveItem(which);
+ }
+ }
+ }
+
+ /**
+ * Move an item, bypassing the drag-sort process. Simply calls
+ * through to {@link DropListener#drop(int, int)}.
+ *
+ * @param from Position to move (NOTE: headers/footers ignored!
+ * this is a position in your input ListAdapter).
+ * @param to Target position (NOTE: headers/footers ignored!
+ * this is a position in your input ListAdapter).
+ */
+ public void moveItem(int from, int to) {
+ if (mDropListener != null) {
+ final int count = getInputAdapter().getCount();
+ if (from >= 0 && from < count && to >= 0 && to < count) {
+ mDropListener.drop(from, to);
+ }
+ }
+ }
+
+ /**
+ * Cancel a drag. Calls {@link #stopDrag(boolean, boolean)} with
+ * <code>true</code> as the first argument.
+ */
+ public void cancelDrag() {
+ if (mDragState == DRAGGING) {
+ mDragScroller.stopScrolling(true);
+ destroyFloatView();
+ clearPositions();
+ adjustAllItems();
+
+ if (mInTouchEvent) {
+ mDragState = STOPPED;
+ } else {
+ mDragState = IDLE;
+ }
+ }
+ }
+
+ private void clearPositions() {
+ mSrcPos = -1;
+ mFirstExpPos = -1;
+ mSecondExpPos = -1;
+ mFloatPos = -1;
+ }
+
+ private void dropFloatView() {
+ // must set to avoid cancelDrag being called from the
+ // DataSetObserver
+ mDragState = DROPPING;
+
+ if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) {
+ final int numHeaders = getHeaderViewsCount();
+ mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders);
+ }
+
+ destroyFloatView();
+
+ adjustOnReorder();
+ clearPositions();
+ adjustAllItems();
+
+ // now the drag is done
+ if (mInTouchEvent) {
+ mDragState = STOPPED;
+ } else {
+ mDragState = IDLE;
+ }
+ }
+
+ private void doRemoveItem() {
+ doRemoveItem(mSrcPos - getHeaderViewsCount());
+ }
+
+ /**
+ * Removes dragged item from the list. Calls RemoveListener.
+ */
+ private void doRemoveItem(int which) {
+ // must set to avoid cancelDrag being called from the
+ // DataSetObserver
+ mDragState = REMOVING;
+
+ // end it
+ if (mRemoveListener != null) {
+ mRemoveListener.remove(which);
+ }
+
+ destroyFloatView();
+
+ adjustOnReorder();
+ clearPositions();
+
+ // now the drag is done
+ if (mInTouchEvent) {
+ mDragState = STOPPED;
+ } else {
+ mDragState = IDLE;
+ }
+ }
+
+ private void adjustOnReorder() {
+ final int firstPos = getFirstVisiblePosition();
+ // Log.d("mobeta", "first="+firstPos+" src="+mSrcPos);
+ if (mSrcPos < firstPos) {
+ // collapsed src item is off screen;
+ // adjust the scroll after item heights have been fixed
+ View v = getChildAt(0);
+ int top = 0;
+ if (v != null) {
+ top = v.getTop();
+ }
+ // Log.d("mobeta", "top="+top+" fvh="+mFloatViewHeight);
+ setSelectionFromTop(firstPos - 1, top - getPaddingTop());
+ }
+ }
+
+ /**
+ * Stop a drag in progress. Pass <code>true</code> if you would
+ * like to remove the dragged item from the list.
+ *
+ * @param remove Remove the dragged item from the list. Calls
+ * a registered RemoveListener, if one exists. Otherwise, calls
+ * the DropListener, if one exists.
+ *
+ * @return True if the stop was successful. False if there is
+ * no floating View.
+ */
+ public boolean stopDrag(boolean remove) {
+ mUseRemoveVelocity = false;
+ return stopDrag(remove, 0);
+ }
+
+ public boolean stopDragWithVelocity(boolean remove, float velocityX) {
+
+ mUseRemoveVelocity = true;
+ return stopDrag(remove, velocityX);
+ }
+
+ public boolean stopDrag(boolean remove, float velocityX) {
+ if (mFloatView != null) {
+ mDragScroller.stopScrolling(true);
+
+ if (remove) {
+ removeItem(mSrcPos - getHeaderViewsCount(), velocityX);
+ } else {
+ if (mDropAnimator != null) {
+ mDropAnimator.start();
+ } else {
+ dropFloatView();
+ }
+ }
+
+ if (mTrackDragSort) {
+ mDragSortTracker.stopTracking();
+ }
+
+ return true;
+ } else {
+ // stop failed
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mIgnoreTouchEvent) {
+ mIgnoreTouchEvent = false;
+ return false;
+ }
+
+ if (!mDragEnabled) {
+ return super.onTouchEvent(ev);
+ }
+
+ boolean more = false;
+
+ boolean lastCallWasIntercept = mLastCallWasIntercept;
+ mLastCallWasIntercept = false;
+
+ if (!lastCallWasIntercept) {
+ saveTouchCoords(ev);
+ }
+
+ // if (mFloatView != null) {
+ if (mDragState == DRAGGING) {
+ onDragTouchEvent(ev);
+ more = true; // give us more!
+ } else {
+ // what if float view is null b/c we dropped in middle
+ // of drag touch event?
+
+ // if (mDragState != STOPPED) {
+ if (mDragState == IDLE) {
+ if (super.onTouchEvent(ev)) {
+ more = true;
+ }
+ }
+
+ int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ switch (action) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ doActionUpOrCancel();
+ break;
+ default:
+ if (more) {
+ mCancelMethod = ON_TOUCH_EVENT;
+ }
+ }
+ }
+
+ return more;
+ }
+
+ private void doActionUpOrCancel() {
+ mCancelMethod = NO_CANCEL;
+ mInTouchEvent = false;
+ if (mDragState == STOPPED) {
+ mDragState = IDLE;
+ }
+ mCurrFloatAlpha = mFloatAlpha;
+ mListViewIntercepted = false;
+ mChildHeightCache.clear();
+ }
+
+ private void saveTouchCoords(MotionEvent ev) {
+ int action = ev.getAction() & MotionEvent.ACTION_MASK;
+ if (action != MotionEvent.ACTION_DOWN) {
+ mLastX = mX;
+ mLastY = mY;
+ }
+ mX = (int) ev.getX();
+ mY = (int) ev.getY();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mLastX = mX;
+ mLastY = mY;
+ }
+ mOffsetX = (int) ev.getRawX() - mX;
+ mOffsetY = (int) ev.getRawY() - mY;
+ }
+
+ public boolean listViewIntercepted() {
+ return mListViewIntercepted;
+ }
+
+ private boolean mListViewIntercepted = false;
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (!mDragEnabled) {
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ saveTouchCoords(ev);
+ mLastCallWasIntercept = true;
+
+ int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ if (mDragState != IDLE) {
+ // intercept and ignore
+ mIgnoreTouchEvent = true;
+ return true;
+ }
+ mInTouchEvent = true;
+ }
+
+ boolean intercept = false;
+
+ // the following deals with calls to super.onInterceptTouchEvent
+ if (mFloatView != null) {
+ // super's touch event canceled in startDrag
+ intercept = true;
+ } else {
+ if (super.onInterceptTouchEvent(ev)) {
+ mListViewIntercepted = true;
+ intercept = true;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ doActionUpOrCancel();
+ break;
+ default:
+ if (intercept) {
+ mCancelMethod = ON_TOUCH_EVENT;
+ } else {
+ mCancelMethod = ON_INTERCEPT_TOUCH_EVENT;
+ }
+ }
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mInTouchEvent = false;
+ }
+
+ return intercept;
+ }
+
+ /**
+ * Set the width of each drag scroll region by specifying
+ * a fraction of the ListView height.
+ *
+ * @param heightFraction Fraction of ListView height. Capped at
+ * 0.5f.
+ *
+ */
+ public void setDragScrollStart(float heightFraction) {
+ setDragScrollStarts(heightFraction, heightFraction);
+ }
+
+ /**
+ * Set the width of each drag scroll region by specifying
+ * a fraction of the ListView height.
+ *
+ * @param upperFrac Fraction of ListView height for up-scroll bound.
+ * Capped at 0.5f.
+ * @param lowerFrac Fraction of ListView height for down-scroll bound.
+ * Capped at 0.5f.
+ *
+ */
+ public void setDragScrollStarts(float upperFrac, float lowerFrac) {
+ if (lowerFrac > 0.5f) {
+ mDragDownScrollStartFrac = 0.5f;
+ } else {
+ mDragDownScrollStartFrac = lowerFrac;
+ }
+
+ if (upperFrac > 0.5f) {
+ mDragUpScrollStartFrac = 0.5f;
+ } else {
+ mDragUpScrollStartFrac = upperFrac;
+ }
+
+ if (getHeight() != 0) {
+ updateScrollStarts();
+ }
+ }
+
+ private void continueDrag(int x, int y) {
+
+ // proposed position
+ mFloatLoc.x = x - mDragDeltaX;
+ mFloatLoc.y = y - mDragDeltaY;
+
+ doDragFloatView(true);
+
+ int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf);
+ int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf);
+
+ // get the current scroll direction
+ int currentScrollDir = mDragScroller.getScrollDir();
+
+ if (minY > mLastY && minY > mDownScrollStartY && currentScrollDir != DragScroller.DOWN) {
+ // dragged down, it is below the down scroll start and it is not
+ // scrolling up
+
+ if (currentScrollDir != DragScroller.STOP) {
+ // moved directly from up scroll to down scroll
+ mDragScroller.stopScrolling(true);
+ }
+
+ // start scrolling down
+ mDragScroller.startScrolling(DragScroller.DOWN);
+ } else if (maxY < mLastY && maxY < mUpScrollStartY && currentScrollDir != DragScroller.UP) {
+ // dragged up, it is above the up scroll start and it is not
+ // scrolling up
+
+ if (currentScrollDir != DragScroller.STOP) {
+ // moved directly from down scroll to up scroll
+ mDragScroller.stopScrolling(true);
+ }
+
+ // start scrolling up
+ mDragScroller.startScrolling(DragScroller.UP);
+ }
+ else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY
+ && mDragScroller.isScrolling()) {
+ // not in the upper nor in the lower drag-scroll regions but it is
+ // still scrolling
+
+ mDragScroller.stopScrolling(true);
+ }
+ }
+
+ private void updateScrollStarts() {
+ final int padTop = getPaddingTop();
+ final int listHeight = getHeight() - padTop - getPaddingBottom();
+ float heightF = (float) listHeight;
+
+ mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF;
+ mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * heightF;
+
+ mUpScrollStartY = (int) mUpScrollStartYF;
+ mDownScrollStartY = (int) mDownScrollStartYF;
+
+ mDragUpScrollHeight = mUpScrollStartYF - padTop;
+ mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateScrollStarts();
+ }
+
+ private void adjustAllItems() {
+ final int first = getFirstVisiblePosition();
+ final int last = getLastVisiblePosition();
+
+ int begin = Math.max(0, getHeaderViewsCount() - first);
+ int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first);
+
+ for (int i = begin; i <= end; ++i) {
+ View v = getChildAt(i);
+ if (v != null) {
+ adjustItem(first + i, v, false);
+ }
+ }
+ }
+
+ private void adjustItem(int position) {
+ View v = getChildAt(position - getFirstVisiblePosition());
+
+ if (v != null) {
+ adjustItem(position, v, false);
+ }
+ }
+
+ /**
+ * Sets layout param height, gravity, and visibility on
+ * wrapped item.
+ */
+ private void adjustItem(int position, View v, boolean invalidChildHeight) {
+
+ // Adjust item height
+ ViewGroup.LayoutParams lp = v.getLayoutParams();
+ int height;
+ if (position != mSrcPos && position != mFirstExpPos && position != mSecondExpPos) {
+ height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ } else {
+ height = calcItemHeight(position, v, invalidChildHeight);
+ }
+
+ if (height != lp.height) {
+ lp.height = height;
+ v.setLayoutParams(lp);
+ }
+
+ // Adjust item gravity
+ if (position == mFirstExpPos || position == mSecondExpPos) {
+ if (position < mSrcPos) {
+ ((DragSortItemView) v).setGravity(Gravity.BOTTOM);
+ } else if (position > mSrcPos) {
+ ((DragSortItemView) v).setGravity(Gravity.TOP);
+ }
+ }
+
+ // Finally adjust item visibility
+
+ int oldVis = v.getVisibility();
+ int vis = View.VISIBLE;
+
+ if (position == mSrcPos && mFloatView != null) {
+ vis = View.INVISIBLE;
+ }
+
+ if (vis != oldVis) {
+ v.setVisibility(vis);
+ }
+ }
+
+ private int getChildHeight(int position) {
+ if (position == mSrcPos) {
+ return 0;
+ }
+
+ View v = getChildAt(position - getFirstVisiblePosition());
+
+ if (v != null) {
+ // item is onscreen, therefore child height is valid,
+ // hence the "true"
+ return getChildHeight(position, v, false);
+ } else {
+ // item is offscreen
+ // first check cache for child height at this position
+ int childHeight = mChildHeightCache.get(position);
+ if (childHeight != -1) {
+ // Log.d("mobeta", "found child height in cache!");
+ return childHeight;
+ }
+
+ final ListAdapter adapter = getAdapter();
+ int type = adapter.getItemViewType(position);
+
+ // There might be a better place for checking for the following
+ final int typeCount = adapter.getViewTypeCount();
+ if (typeCount != mSampleViewTypes.length) {
+ mSampleViewTypes = new View[typeCount];
+ }
+
+ if (type >= 0) {
+ if (mSampleViewTypes[type] == null) {
+ v = adapter.getView(position, null, this);
+ mSampleViewTypes[type] = v;
+ } else {
+ v = adapter.getView(position, mSampleViewTypes[type], this);
+ }
+ } else {
+ // type is HEADER_OR_FOOTER or IGNORE
+ v = adapter.getView(position, null, this);
+ }
+
+ // current child height is invalid, hence "true" below
+ childHeight = getChildHeight(position, v, true);
+
+ // cache it because this could have been expensive
+ mChildHeightCache.add(position, childHeight);
+
+ return childHeight;
+ }
+ }
+
+ private int getChildHeight(int position, View item, boolean invalidChildHeight) {
+ if (position == mSrcPos) {
+ return 0;
+ }
+
+ View child;
+ if (position < getHeaderViewsCount() || position >= getCount() - getFooterViewsCount()) {
+ child = item;
+ } else {
+ child = ((ViewGroup) item).getChildAt(0);
+ }
+
+ ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+ if (lp != null) {
+ if (lp.height > 0) {
+ return lp.height;
+ }
+ }
+
+ int childHeight = child.getHeight();
+
+ if (childHeight == 0 || invalidChildHeight) {
+ measureItem(child);
+ childHeight = child.getMeasuredHeight();
+ }
+
+ return childHeight;
+ }
+
+ private int calcItemHeight(int position, View item, boolean invalidChildHeight) {
+ return calcItemHeight(position, getChildHeight(position, item, invalidChildHeight));
+ }
+
+ private int calcItemHeight(int position, int childHeight) {
+
+ int divHeight = getDividerHeight();
+
+ boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos;
+ int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
+ int slideHeight = (int) (mSlideFrac * maxNonSrcBlankHeight);
+
+ int height;
+
+ if (position == mSrcPos) {
+ if (mSrcPos == mFirstExpPos) {
+ if (isSliding) {
+ height = slideHeight + mItemHeightCollapsed;
+ } else {
+ height = mFloatViewHeight;
+ }
+ } else if (mSrcPos == mSecondExpPos) {
+ // if gets here, we know an item is sliding
+ height = mFloatViewHeight - slideHeight;
+ } else {
+ height = mItemHeightCollapsed;
+ }
+ } else if (position == mFirstExpPos) {
+ if (isSliding) {
+ height = childHeight + slideHeight;
+ } else {
+ height = childHeight + maxNonSrcBlankHeight;
+ }
+ } else if (position == mSecondExpPos) {
+ // we know an item is sliding (b/c 2ndPos != 1stPos)
+ height = childHeight + maxNonSrcBlankHeight - slideHeight;
+ } else {
+ height = childHeight;
+ }
+
+ return height;
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mBlockLayoutRequests) {
+ super.requestLayout();
+ }
+ }
+
+ private int adjustScroll(int movePos, View moveItem, int oldFirstExpPos, int oldSecondExpPos) {
+ int adjust = 0;
+
+ final int childHeight = getChildHeight(movePos);
+
+ int moveHeightBefore = moveItem.getHeight();
+ int moveHeightAfter = calcItemHeight(movePos, childHeight);
+
+ int moveBlankBefore = moveHeightBefore;
+ int moveBlankAfter = moveHeightAfter;
+ if (movePos != mSrcPos) {
+ moveBlankBefore -= childHeight;
+ moveBlankAfter -= childHeight;
+ }
+
+ int maxBlank = mFloatViewHeight;
+ if (mSrcPos != mFirstExpPos && mSrcPos != mSecondExpPos) {
+ maxBlank -= mItemHeightCollapsed;
+ }
+
+ if (movePos <= oldFirstExpPos) {
+ if (movePos > mFirstExpPos) {
+ adjust += maxBlank - moveBlankAfter;
+ }
+ } else if (movePos == oldSecondExpPos) {
+ if (movePos <= mFirstExpPos) {
+ adjust += moveBlankBefore - maxBlank;
+ } else if (movePos == mSecondExpPos) {
+ adjust += moveHeightBefore - moveHeightAfter;
+ } else {
+ adjust += moveBlankBefore;
+ }
+ } else {
+ if (movePos <= mFirstExpPos) {
+ adjust -= maxBlank;
+ } else if (movePos == mSecondExpPos) {
+ adjust -= moveBlankAfter;
+ }
+ }
+
+ return adjust;
+ }
+
+ private void measureItem(View item) {
+ ViewGroup.LayoutParams lp = item.getLayoutParams();
+ if (lp == null) {
+ lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ item.setLayoutParams(lp);
+ }
+ int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft()
+ + getListPaddingRight(), lp.width);
+ int hspec;
+ if (lp.height > 0) {
+ hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+ } else {
+ hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+ item.measure(wspec, hspec);
+ }
+
+ private void measureFloatView() {
+ if (mFloatView != null) {
+ measureItem(mFloatView);
+ mFloatViewHeight = mFloatView.getMeasuredHeight();
+ mFloatViewHeightHalf = mFloatViewHeight / 2;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ // Log.d("mobeta", "onMeasure called");
+ if (mFloatView != null) {
+ if (mFloatView.isLayoutRequested()) {
+ measureFloatView();
+ }
+ mFloatViewOnMeasured = true; // set to false after layout
+ }
+ mWidthMeasureSpec = widthMeasureSpec;
+ }
+
+ @Override
+ protected void layoutChildren() {
+ super.layoutChildren();
+
+ if (mFloatView != null) {
+ if (mFloatView.isLayoutRequested() && !mFloatViewOnMeasured) {
+ // Have to measure here when usual android measure
+ // pass is skipped. This happens during a drag-sort
+ // when layoutChildren is called directly.
+ measureFloatView();
+ }
+ mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight());
+ mFloatViewOnMeasured = false;
+ }
+ }
+
+ protected boolean onDragTouchEvent(MotionEvent ev) {
+ // we are in a drag
+ int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ switch (ev.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_CANCEL:
+ if (mDragState == DRAGGING) {
+ cancelDrag();
+ }
+ doActionUpOrCancel();
+ break;
+ case MotionEvent.ACTION_UP:
+ // Log.d("mobeta", "calling stopDrag from onDragTouchEvent");
+ if (mDragState == DRAGGING) {
+ stopDrag(false);
+ }
+ doActionUpOrCancel();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ continueDrag((int) ev.getX(), (int) ev.getY());
+ break;
+ }
+
+ return true;
+ }
+
+ private boolean mFloatViewInvalidated = false;
+
+ private void invalidateFloatView() {
+ mFloatViewInvalidated = true;
+ }
+
+ /**
+ * Start a drag of item at <code>position</code> using the
+ * registered FloatViewManager. Calls through
+ * to {@link #startDrag(int,View,int,int,int)} after obtaining
+ * the floating View from the FloatViewManager.
+ *
+ * @param position Item to drag.
+ * @param dragFlags Flags that restrict some movements of the
+ * floating View. For example, set <code>dragFlags |=
+ * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating
+ * View in all directions except off the screen to the left.
+ * @param deltaX Offset in x of the touch coordinate from the
+ * left edge of the floating View (i.e. touch-x minus float View
+ * left).
+ * @param deltaY Offset in y of the touch coordinate from the
+ * top edge of the floating View (i.e. touch-y minus float View
+ * top).
+ *
+ * @return True if the drag was started, false otherwise. This
+ * <code>startDrag</code> will fail if we are not currently in
+ * a touch event, there is no registered FloatViewManager,
+ * or the FloatViewManager returns a null View.
+ */
+ public boolean startDrag(int position, int dragFlags, int deltaX, int deltaY) {
+ if (!mInTouchEvent || mFloatViewManager == null) {
+ return false;
+ }
+
+ View v = mFloatViewManager.onCreateFloatView(position);
+
+ if (v == null) {
+ return false;
+ } else {
+ return startDrag(position, v, dragFlags, deltaX, deltaY);
+ }
+
+ }
+
+ /**
+ * Start a drag of item at <code>position</code> without using
+ * a FloatViewManager.
+ *
+ * @param position Item to drag.
+ * @param floatView Floating View.
+ * @param dragFlags Flags that restrict some movements of the
+ * floating View. For example, set <code>dragFlags |=
+ * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating
+ * View in all directions except off the screen to the left.
+ * @param deltaX Offset in x of the touch coordinate from the
+ * left edge of the floating View (i.e. touch-x minus float View
+ * left).
+ * @param deltaY Offset in y of the touch coordinate from the
+ * top edge of the floating View (i.e. touch-y minus float View
+ * top).
+ *
+ * @return True if the drag was started, false otherwise. This
+ * <code>startDrag</code> will fail if we are not currently in
+ * a touch event, <code>floatView</code> is null, or there is
+ * a drag in progress.
+ */
+ public boolean startDrag(int position, View floatView, int dragFlags, int deltaX, int deltaY) {
+ if (mDragState != IDLE || !mInTouchEvent || mFloatView != null || floatView == null
+ || !mDragEnabled) {
+ return false;
+ }
+
+ if (getParent() != null) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ }
+
+ int pos = position + getHeaderViewsCount();
+ mFirstExpPos = pos;
+ mSecondExpPos = pos;
+ mSrcPos = pos;
+ mFloatPos = pos;
+
+ // mDragState = dragType;
+ mDragState = DRAGGING;
+ mDragFlags = 0;
+ mDragFlags |= dragFlags;
+
+ mFloatView = floatView;
+ measureFloatView(); // sets mFloatViewHeight
+
+ mDragDeltaX = deltaX;
+ mDragDeltaY = deltaY;
+ mDragStartY = mY;
+
+ // updateFloatView(mX - mDragDeltaX, mY - mDragDeltaY);
+ mFloatLoc.x = mX - mDragDeltaX;
+ mFloatLoc.y = mY - mDragDeltaY;
+
+ // set src item invisible
+ final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition());
+
+ if (srcItem != null) {
+ srcItem.setVisibility(View.INVISIBLE);
+ }
+
+ if (mTrackDragSort) {
+ mDragSortTracker.startTracking();
+ }
+
+ // once float view is created, events are no longer passed
+ // to ListView
+ switch (mCancelMethod) {
+ case ON_TOUCH_EVENT:
+ super.onTouchEvent(mCancelEvent);
+ break;
+ case ON_INTERCEPT_TOUCH_EVENT:
+ super.onInterceptTouchEvent(mCancelEvent);
+ break;
+ }
+
+ requestLayout();
+
+ if (mLiftAnimator != null) {
+ mLiftAnimator.start();
+ }
+
+ return true;
+ }
+
+ private void doDragFloatView(boolean forceInvalidate) {
+ int movePos = getFirstVisiblePosition() + getChildCount() / 2;
+ View moveItem = getChildAt(getChildCount() / 2);
+
+ if (moveItem == null) {
+ return;
+ }
+
+ doDragFloatView(movePos, moveItem, forceInvalidate);
+ }
+
+ private void doDragFloatView(int movePos, View moveItem, boolean forceInvalidate) {
+ mBlockLayoutRequests = true;
+
+ updateFloatView();
+
+ int oldFirstExpPos = mFirstExpPos;
+ int oldSecondExpPos = mSecondExpPos;
+
+ boolean updated = updatePositions();
+
+ if (updated) {
+ adjustAllItems();
+ int scroll = adjustScroll(movePos, moveItem, oldFirstExpPos, oldSecondExpPos);
+ // Log.d("mobeta", " adjust scroll="+scroll);
+
+ setSelectionFromTop(movePos, moveItem.getTop() + scroll - getPaddingTop());
+ layoutChildren();
+ }
+
+ if (updated || forceInvalidate) {
+ invalidate();
+ }
+
+ mBlockLayoutRequests = false;
+ }
+
+ /**
+ * Sets float View location based on suggested values and
+ * constraints set in mDragFlags.
+ */
+ private void updateFloatView() {
+
+ if (mFloatViewManager != null) {
+ mTouchLoc.set(mX, mY);
+ mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, mTouchLoc);
+ }
+
+ final int floatX = mFloatLoc.x;
+ final int floatY = mFloatLoc.y;
+
+ // restrict x motion
+ int padLeft = getPaddingLeft();
+ if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) {
+ mFloatLoc.x = padLeft;
+ } else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) {
+ mFloatLoc.x = padLeft;
+ }
+
+ // keep floating view from going past bottom of last header view
+ final int numHeaders = getHeaderViewsCount();
+ final int numFooters = getFooterViewsCount();
+ final int firstPos = getFirstVisiblePosition();
+ final int lastPos = getLastVisiblePosition();
+
+ // Log.d("mobeta",
+ // "nHead="+numHeaders+" nFoot="+numFooters+" first="+firstPos+" last="+lastPos);
+ int topLimit = getPaddingTop();
+ if (firstPos < numHeaders) {
+ topLimit = getChildAt(numHeaders - firstPos - 1).getBottom();
+ }
+ if ((mDragFlags & DRAG_NEG_Y) == 0) {
+ if (firstPos <= mSrcPos) {
+ topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(), topLimit);
+ }
+ }
+ // bottom limit is top of first footer View or
+ // bottom of last item in list
+ int bottomLimit = getHeight() - getPaddingBottom();
+ if (lastPos >= getCount() - numFooters - 1) {
+ bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos).getBottom();
+ }
+ if ((mDragFlags & DRAG_POS_Y) == 0) {
+ if (lastPos >= mSrcPos) {
+ bottomLimit = Math.min(getChildAt(mSrcPos - firstPos).getBottom(), bottomLimit);
+ }
+ }
+
+ // Log.d("mobeta", "dragView top=" + (y - mDragDeltaY));
+ // Log.d("mobeta", "limit=" + limit);
+ // Log.d("mobeta", "mDragDeltaY=" + mDragDeltaY);
+
+ if (floatY < topLimit) {
+ mFloatLoc.y = topLimit;
+ } else if (floatY + mFloatViewHeight > bottomLimit) {
+ mFloatLoc.y = bottomLimit - mFloatViewHeight;
+ }
+
+ // get y-midpoint of floating view (constrained to ListView bounds)
+ mFloatViewMid = mFloatLoc.y + mFloatViewHeightHalf;
+ }
+
+ private void destroyFloatView() {
+ if (mFloatView != null) {
+ mFloatView.setVisibility(GONE);
+ if (mFloatViewManager != null) {
+ mFloatViewManager.onDestroyFloatView(mFloatView);
+ }
+ mFloatView = null;
+ invalidate();
+ }
+ }
+
+ /**
+ * Interface for customization of the floating View appearance
+ * and dragging behavior. Implement
+ * your own and pass it to {@link #setFloatViewManager}. If
+ * your own is not passed, the default {@link SimpleFloatViewManager}
+ * implementation is used.
+ */
+ public interface FloatViewManager {
+ /**
+ * Return the floating View for item at <code>position</code>.
+ * DragSortListView will measure and layout this View for you,
+ * so feel free to just inflate it. You can help DSLV by
+ * setting some {@link ViewGroup.LayoutParams} on this View;
+ * otherwise it will set some for you (with a width of FILL_PARENT
+ * and a height of WRAP_CONTENT).
+ *
+ * @param position Position of item to drag (NOTE:
+ * <code>position</code> excludes header Views; thus, if you
+ * want to call {@link ListView#getChildAt(int)}, you will need
+ * to add {@link ListView#getHeaderViewsCount()} to the index).
+ *
+ * @return The View you wish to display as the floating View.
+ */
+ public View onCreateFloatView(int position);
+
+ /**
+ * Called whenever the floating View is dragged. Float View
+ * properties can be changed here. Also, the upcoming location
+ * of the float View can be altered by setting
+ * <code>location.x</code> and <code>location.y</code>.
+ *
+ * @param floatView The floating View.
+ * @param location The location (top-left; relative to DSLV
+ * top-left) at which the float
+ * View would like to appear, given the current touch location
+ * and the offset provided in {@link DragSortListView#startDrag}.
+ * @param touch The current touch location (relative to DSLV
+ * top-left).
+ * @param pendingScroll
+ */
+ public void onDragFloatView(View floatView, Point location, Point touch);
+
+ /**
+ * Called when the float View is dropped; lets you perform
+ * any necessary cleanup. The internal DSLV floating View
+ * reference is set to null immediately after this is called.
+ *
+ * @param floatView The floating View passed to
+ * {@link #onCreateFloatView(int)}.
+ */
+ public void onDestroyFloatView(View floatView);
+ }
+
+ public void setFloatViewManager(FloatViewManager manager) {
+ mFloatViewManager = manager;
+ }
+
+ public void setDragListener(DragListener l) {
+ mDragListener = l;
+ }
+
+ /**
+ * Allows for easy toggling between a DragSortListView
+ * and a regular old ListView. If enabled, items are
+ * draggable, where the drag init mode determines how
+ * items are lifted (see {@link setDragInitMode(int)}).
+ * If disabled, items cannot be dragged.
+ *
+ * @param enabled Set <code>true</code> to enable list
+ * item dragging
+ */
+ public void setDragEnabled(boolean enabled) {
+ mDragEnabled = enabled;
+ }
+
+ public boolean isDragEnabled() {
+ return mDragEnabled;
+ }
+
+ /**
+ * This better reorder your ListAdapter! DragSortListView does not do this
+ * for you; doesn't make sense to. Make sure
+ * {@link BaseAdapter#notifyDataSetChanged()} or something like it is called
+ * in your implementation. Furthermore, if you have a choiceMode other than
+ * none and the ListAdapter does not return true for
+ * {@link ListAdapter#hasStableIds()}, you will need to call
+ * {@link #moveCheckState(int, int)} to move the check boxes along with the
+ * list items.
+ *
+ * @param l
+ */
+ public void setDropListener(DropListener l) {
+ mDropListener = l;
+ }
+
+ /**
+ * Probably a no-brainer, but make sure that your remove listener
+ * calls {@link BaseAdapter#notifyDataSetChanged()} or something like it.
+ * When an item removal occurs, DragSortListView
+ * relies on a redraw of all the items to recover invisible views
+ * and such. Strictly speaking, if you remove something, your dataset
+ * has changed...
+ *
+ * @param l
+ */
+ public void setRemoveListener(RemoveListener l) {
+ mRemoveListener = l;
+ }
+
+ public interface DragListener {
+ public void drag(int from, int to);
+ }
+
+ /**
+ * Your implementation of this has to reorder your ListAdapter!
+ * Make sure to call
+ * {@link BaseAdapter#notifyDataSetChanged()} or something like it
+ * in your implementation.
+ *
+ * @author heycosmo
+ *
+ */
+ public interface DropListener {
+ public void drop(int from, int to);
+ }
+
+ /**
+ * Make sure to call
+ * {@link BaseAdapter#notifyDataSetChanged()} or something like it
+ * in your implementation.
+ *
+ * @author heycosmo
+ *
+ */
+ public interface RemoveListener {
+ public void remove(int which);
+ }
+
+ public interface DragSortListener extends DropListener, DragListener, RemoveListener {
+ }
+
+ public void setDragSortListener(DragSortListener l) {
+ setDropListener(l);
+ setDragListener(l);
+ setRemoveListener(l);
+ }
+
+ /**
+ * Completely custom scroll speed profile. Default increases linearly
+ * with position and is constant in time. Create your own by implementing
+ * {@link DragSortListView.DragScrollProfile}.
+ *
+ * @param ssp
+ */
+ public void setDragScrollProfile(DragScrollProfile ssp) {
+ if (ssp != null) {
+ mScrollProfile = ssp;
+ }
+ }
+
+ /**
+ * Use this to move the check state of an item from one position to another
+ * in a drop operation. If you have a choiceMode which is not none, this
+ * method must be called when the order of items changes in an underlying
+ * adapter which does not have stable IDs (see
+ * {@link ListAdapter#hasStableIds()}). This is because without IDs, the
+ * ListView has no way of knowing which items have moved where, and cannot
+ * update the check state accordingly.
+ * <p>
+ * A word of warning about a "feature" in Android that you may run into when
+ * dealing with movable list items: for an adapter that <em>does</em> have
+ * stable IDs, ListView will attempt to locate each item based on its ID and
+ * move the check state from the item's old position to the new position —
+ * which is all fine and good (and removes the need for calling this
+ * function), except for the half-baked approach. Apparently to save time in
+ * the naive algorithm used, ListView will only search for an ID in the
+ * close neighborhood of the old position. If the user moves an item too far
+ * (specifically, more than 20 rows away), ListView will give up and just
+ * force the item to be unchecked. So if there is a reasonable chance that
+ * the user will move items more than 20 rows away from the original
+ * position, you may wish to use an adapter with unstable IDs and call this
+ * method manually instead.
+ *
+ * @param from
+ * @param to
+ */
+ public void moveCheckState(int from, int to) {
+ // This method runs in O(n log n) time (n being the number of list
+ // items). The bottleneck is the call to AbsListView.setItemChecked,
+ // which is O(log n) because of the binary search involved in calling
+ // SparseBooleanArray.put().
+ //
+ // To improve on the average time, we minimize the number of calls to
+ // setItemChecked by only calling it for items that actually have a
+ // changed state. This is achieved by building a list containing the
+ // start and end of the "runs" of checked items, and then moving the
+ // runs. Note that moving an item from A to B is essentially a rotation
+ // of the range of items in [A, B]. Let's say we have
+ // . . U V X Y Z . .
+ // and move U after Z. This is equivalent to a rotation one step to the
+ // left within the range you are moving across:
+ // . . V X Y Z U . .
+ //
+ // So, to perform the move we enumerate all the runs within the move
+ // range, then rotate each run one step to the left or right (depending
+ // on move direction). For example, in the list:
+ // X X . X X X . X
+ // we have two runs. One begins at the last item of the list and wraps
+ // around to the beginning, ending at position 1. The second begins at
+ // position 3 and ends at position 5. To rotate a run, regardless of
+ // length, we only need to set a check mark at one end of the run, and
+ // clear a check mark at the other end:
+ // X . X X X . X X
+ SparseBooleanArray cip = getCheckedItemPositions();
+ int rangeStart = from;
+ int rangeEnd = to;
+ if (to < from) {
+ rangeStart = to;
+ rangeEnd = from;
+ }
+ rangeEnd += 1;
+
+ int[] runStart = new int[cip.size()];
+ int[] runEnd = new int[cip.size()];
+ int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd);
+ if (runCount == 1 && (runStart[0] == runEnd[0])) {
+ // Special case where all items are checked, we can never set any
+ // item to false like we do below.
+ return;
+ }
+
+ if (from < to) {
+ for (int i = 0; i != runCount; i++) {
+ setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true);
+ setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false);
+ }
+
+ } else {
+ for (int i = 0; i != runCount; i++) {
+ setItemChecked(runStart[i], false);
+ setItemChecked(runEnd[i], true);
+ }
+ }
+ }
+
+ /**
+ * Use this when an item has been deleted, to move the check state of all
+ * following items up one step. If you have a choiceMode which is not none,
+ * this method must be called when the order of items changes in an
+ * underlying adapter which does not have stable IDs (see
+ * {@link ListAdapter#hasStableIds()}). This is because without IDs, the
+ * ListView has no way of knowing which items have moved where, and cannot
+ * update the check state accordingly.
+ *
+ * See also further comments on {@link #moveCheckState(int, int)}.
+ *
+ * @param position
+ */
+ public void removeCheckState(int position) {
+ SparseBooleanArray cip = getCheckedItemPositions();
+
+ if (cip.size() == 0)
+ return;
+ int[] runStart = new int[cip.size()];
+ int[] runEnd = new int[cip.size()];
+ int rangeStart = position;
+ int rangeEnd = cip.keyAt(cip.size() - 1) + 1;
+ int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd);
+ for (int i = 0; i != runCount; i++) {
+ if (!(runStart[i] == position || (runEnd[i] < runStart[i] && runEnd[i] > position))) {
+ // Only set a new check mark in front of this run if it does
+ // not contain the deleted position. If it does, we only need
+ // to make it one check mark shorter at the end.
+ setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true);
+ }
+ setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false);
+ }
+ }
+
+ private static int buildRunList(SparseBooleanArray cip, int rangeStart,
+ int rangeEnd, int[] runStart, int[] runEnd) {
+ int runCount = 0;
+
+ int i = findFirstSetIndex(cip, rangeStart, rangeEnd);
+ if (i == -1)
+ return 0;
+
+ int position = cip.keyAt(i);
+ int currentRunStart = position;
+ int currentRunEnd = currentRunStart + 1;
+ for (i++; i < cip.size() && (position = cip.keyAt(i)) < rangeEnd; i++) {
+ if (!cip.valueAt(i)) // not checked => not interesting
+ continue;
+ if (position == currentRunEnd) {
+ currentRunEnd++;
+ } else {
+ runStart[runCount] = currentRunStart;
+ runEnd[runCount] = currentRunEnd;
+ runCount++;
+ currentRunStart = position;
+ currentRunEnd = position + 1;
+ }
+ }
+
+ if (currentRunEnd == rangeEnd) {
+ // rangeStart and rangeEnd are equivalent positions so to be
+ // consistent we translate them to the same integer value. That way
+ // we can check whether a run covers the entire range by just
+ // checking if the start equals the end position.
+ currentRunEnd = rangeStart;
+ }
+ runStart[runCount] = currentRunStart;
+ runEnd[runCount] = currentRunEnd;
+ runCount++;
+
+ if (runCount > 1) {
+ if (runStart[0] == rangeStart && runEnd[runCount - 1] == rangeStart) {
+ // The last run ends at the end of the range, and the first run
+ // starts at the beginning of the range. So they are actually
+ // part of the same run, except they wrap around the end of the
+ // range. To avoid adjacent runs, we need to merge them.
+ runStart[0] = runStart[runCount - 1];
+ runCount--;
+ }
+ }
+ return runCount;
+ }
+
+ private static int rotate(int value, int offset, int lowerBound, int upperBound) {
+ int windowSize = upperBound - lowerBound;
+
+ value += offset;
+ if (value < lowerBound) {
+ value += windowSize;
+ } else if (value >= upperBound) {
+ value -= windowSize;
+ }
+ return value;
+ }
+
+ private static int findFirstSetIndex(SparseBooleanArray sba, int rangeStart, int rangeEnd) {
+ int size = sba.size();
+ int i = insertionIndexForKey(sba, rangeStart);
+ while (i < size && sba.keyAt(i) < rangeEnd && !sba.valueAt(i))
+ i++;
+ if (i == size || sba.keyAt(i) >= rangeEnd)
+ return -1;
+ return i;
+ }
+
+ private static int insertionIndexForKey(SparseBooleanArray sba, int key) {
+ int low = 0;
+ int high = sba.size();
+ while (high - low > 0) {
+ int middle = (low + high) >> 1;
+ if (sba.keyAt(middle) < key)
+ low = middle + 1;
+ else
+ high = middle;
+ }
+ return low;
+ }
+
+ /**
+ * Interface for controlling
+ * scroll speed as a function of touch position and time. Use
+ * {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to
+ * set custom profile.
+ *
+ * @author heycosmo
+ *
+ */
+ public interface DragScrollProfile {
+ /**
+ * Return a scroll speed in pixels/millisecond. Always return a
+ * positive number.
+ *
+ * @param w Normalized position in scroll region (i.e. w \in [0,1]).
+ * Small w typically means slow scrolling.
+ * @param t Time (in milliseconds) since start of scroll (handy if you
+ * want scroll acceleration).
+ * @return Scroll speed at position w and time t in pixels/ms.
+ */
+ float getSpeed(float w, long t);
+ }
+
+ private class DragScroller implements Runnable {
+
+ private boolean mAbort;
+
+ private long mPrevTime;
+ private long mCurrTime;
+
+ private int dy;
+ private float dt;
+ private long tStart;
+ private int scrollDir;
+
+ public final static int STOP = -1;
+ public final static int UP = 0;
+ public final static int DOWN = 1;
+
+ private float mScrollSpeed; // pixels per ms
+
+ private boolean mScrolling = false;
+
+ private int mLastHeader;
+ private int mFirstFooter;
+
+ public boolean isScrolling() {
+ return mScrolling;
+ }
+
+ public int getScrollDir() {
+ return mScrolling ? scrollDir : STOP;
+ }
+
+ public DragScroller() {
+ }
+
+ public void startScrolling(int dir) {
+ if (!mScrolling) {
+ // Debug.startMethodTracing("dslv-scroll");
+ mAbort = false;
+ mScrolling = true;
+ tStart = SystemClock.uptimeMillis();
+ mPrevTime = tStart;
+ scrollDir = dir;
+ post(this);
+ }
+ }
+
+ public void stopScrolling(boolean now) {
+ if (now) {
+ DragSortListView.this.removeCallbacks(this);
+ mScrolling = false;
+ } else {
+ mAbort = true;
+ }
+
+ // Debug.stopMethodTracing();
+ }
+
+ @Override
+ public void run() {
+ if (mAbort) {
+ mScrolling = false;
+ return;
+ }
+
+ // Log.d("mobeta", "scroll");
+
+ final int first = getFirstVisiblePosition();
+ final int last = getLastVisiblePosition();
+ final int count = getCount();
+ final int padTop = getPaddingTop();
+ final int listHeight = getHeight() - padTop - getPaddingBottom();
+
+ int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf);
+ int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf);
+
+ if (scrollDir == UP) {
+ View v = getChildAt(0);
+ // Log.d("mobeta", "vtop="+v.getTop()+" padtop="+padTop);
+ if (v == null) {
+ mScrolling = false;
+ return;
+ } else {
+ if (first == 0 && v.getTop() == padTop) {
+ mScrolling = false;
+ return;
+ }
+ }
+ mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY)
+ / mDragUpScrollHeight, mPrevTime);
+ } else {
+ View v = getChildAt(last - first);
+ if (v == null) {
+ mScrolling = false;
+ return;
+ } else {
+ if (last == count - 1 && v.getBottom() <= listHeight + padTop) {
+ mScrolling = false;
+ return;
+ }
+ }
+ mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF)
+ / mDragDownScrollHeight, mPrevTime);
+ }
+
+ mCurrTime = SystemClock.uptimeMillis();
+ dt = (float) (mCurrTime - mPrevTime);
+
+ // dy is change in View position of a list item; i.e. positive dy
+ // means user is scrolling up (list item moves down the screen,
+ // remember
+ // y=0 is at top of View).
+ dy = (int) Math.round(mScrollSpeed * dt);
+
+ int movePos;
+ if (dy >= 0) {
+ dy = Math.min(listHeight, dy);
+ movePos = first;
+ } else {
+ dy = Math.max(-listHeight, dy);
+ movePos = last;
+ }
+
+ final View moveItem = getChildAt(movePos - first);
+ int top = moveItem.getTop() + dy;
+
+ if (movePos == 0 && top > padTop) {
+ top = padTop;
+ }
+
+ // always do scroll
+ mBlockLayoutRequests = true;
+
+ setSelectionFromTop(movePos, top - padTop);
+ DragSortListView.this.layoutChildren();
+ invalidate();
+
+ mBlockLayoutRequests = false;
+
+ // scroll means relative float View movement
+ doDragFloatView(movePos, moveItem, false);
+
+ mPrevTime = mCurrTime;
+ // Log.d("mobeta", " updated prevTime="+mPrevTime);
+
+ post(this);
+ }
+ }
+
+ private class DragSortTracker {
+ StringBuilder mBuilder = new StringBuilder();
+
+ File mFile;
+
+ private int mNumInBuffer = 0;
+ private int mNumFlushes = 0;
+
+ private boolean mTracking = false;
+
+ public DragSortTracker() {
+ File root = Environment.getExternalStorageDirectory();
+ mFile = new File(root, "dslv_state.txt");
+
+ if (!mFile.exists()) {
+ try {
+ mFile.createNewFile();
+ Log.d("mobeta", "file created");
+ } catch (IOException e) {
+ Log.w("mobeta", "Could not create dslv_state.txt");
+ Log.d("mobeta", e.getMessage());
+ }
+ }
+
+ }
+
+ public void startTracking() {
+ mBuilder.append("<DSLVStates>\n");
+ mNumFlushes = 0;
+ mTracking = true;
+ }
+
+ public void appendState() {
+ if (!mTracking) {
+ return;
+ }
+
+ mBuilder.append("<DSLVState>\n");
+ final int children = getChildCount();
+ final int first = getFirstVisiblePosition();
+ mBuilder.append(" <Positions>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(first + i).append(",");
+ }
+ mBuilder.append("</Positions>\n");
+
+ mBuilder.append(" <Tops>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(getChildAt(i).getTop()).append(",");
+ }
+ mBuilder.append("</Tops>\n");
+ mBuilder.append(" <Bottoms>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(getChildAt(i).getBottom()).append(",");
+ }
+ mBuilder.append("</Bottoms>\n");
+
+ mBuilder.append(" <FirstExpPos>").append(mFirstExpPos).append("</FirstExpPos>\n");
+ mBuilder.append(" <FirstExpBlankHeight>")
+ .append(getItemHeight(mFirstExpPos) - getChildHeight(mFirstExpPos))
+ .append("</FirstExpBlankHeight>\n");
+ mBuilder.append(" <SecondExpPos>").append(mSecondExpPos).append("</SecondExpPos>\n");
+ mBuilder.append(" <SecondExpBlankHeight>")
+ .append(getItemHeight(mSecondExpPos) - getChildHeight(mSecondExpPos))
+ .append("</SecondExpBlankHeight>\n");
+ mBuilder.append(" <SrcPos>").append(mSrcPos).append("</SrcPos>\n");
+ mBuilder.append(" <SrcHeight>").append(mFloatViewHeight + getDividerHeight())
+ .append("</SrcHeight>\n");
+ mBuilder.append(" <ViewHeight>").append(getHeight()).append("</ViewHeight>\n");
+ mBuilder.append(" <LastY>").append(mLastY).append("</LastY>\n");
+ mBuilder.append(" <FloatY>").append(mFloatViewMid).append("</FloatY>\n");
+ mBuilder.append(" <ShuffleEdges>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(",");
+ }
+ mBuilder.append("</ShuffleEdges>\n");
+
+ mBuilder.append("</DSLVState>\n");
+ mNumInBuffer++;
+
+ if (mNumInBuffer > 1000) {
+ flush();
+ mNumInBuffer = 0;
+ }
+ }
+
+ public void flush() {
+ if (!mTracking) {
+ return;
+ }
+
+ // save to file on sdcard
+ try {
+ boolean append = true;
+ if (mNumFlushes == 0) {
+ append = false;
+ }
+ FileWriter writer = new FileWriter(mFile, append);
+
+ writer.write(mBuilder.toString());
+ mBuilder.delete(0, mBuilder.length());
+
+ writer.flush();
+ writer.close();
+
+ mNumFlushes++;
+ } catch (IOException e) {
+ // do nothing
+ }
+ }
+
+ public void stopTracking() {
+ if (mTracking) {
+ mBuilder.append("</DSLVStates>\n");
+ flush();
+ mTracking = false;
+ }
+ }
+
+ }
+
+}
diff --git a/ring-android/src/cx/ring/views/dragsortlv/ResourceDragSortCursorAdapter.java b/ring-android/src/cx/ring/views/dragsortlv/ResourceDragSortCursorAdapter.java
new file mode 100644
index 0000000..6bec6a2
--- /dev/null
+++ b/ring-android/src/cx/ring/views/dragsortlv/ResourceDragSortCursorAdapter.java
@@ -0,0 +1,133 @@
+/*
+ * 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 cx.ring.views.dragsortlv;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+
+// taken from v4 rev. 10 ResourceCursorAdapter.java
+
+/**
+ * Static library support version of the framework's {@link android.widget.ResourceCursorAdapter}.
+ * Used to write apps that run on platforms prior to Android 3.0. When running
+ * on Android 3.0 or above, this implementation is still used; it does not try
+ * to switch to the framework's implementation. See the framework SDK
+ * documentation for a class overview.
+ */
+public abstract class ResourceDragSortCursorAdapter extends DragSortCursorAdapter {
+ private int mLayout;
+
+ private int mDropDownLayout;
+
+ private LayoutInflater mInflater;
+
+ /**
+ * Constructor the enables auto-requery.
+ *
+ * @deprecated This option is discouraged, as it results in Cursor queries
+ * being performed on the application's UI thread and thus can cause poor
+ * responsiveness or even Application Not Responding errors. As an alternative,
+ * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
+ *
+ * @param context The context where the ListView associated with this adapter is running
+ * @param layout resource identifier of a layout file that defines the views
+ * for this list item. Unless you override them later, this will
+ * define both the item views and the drop down views.
+ */
+ @Deprecated
+ public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c) {
+ super(context, c);
+ mLayout = mDropDownLayout = layout;
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ /**
+ * Constructor with default behavior as per
+ * {@link CursorAdapter#CursorAdapter(Context, Cursor, boolean)}; it is recommended
+ * you not use this, but instead {@link #ResourceCursorAdapter(Context, int, Cursor, int)}.
+ * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
+ * will always be set.
+ *
+ * @param context The context where the ListView associated with this adapter is running
+ * @param layout resource identifier of a layout file that defines the views
+ * for this list item. Unless you override them later, this will
+ * define both the item views and the drop down views.
+ * @param c The cursor from which to get the data.
+ * @param autoRequery If true the adapter will call requery() on the
+ * cursor whenever it changes so the most recent
+ * data is always displayed. Using true here is discouraged.
+ */
+ public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c, boolean autoRequery) {
+ super(context, c, autoRequery);
+ mLayout = mDropDownLayout = layout;
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ /**
+ * Standard constructor.
+ *
+ * @param context The context where the ListView associated with this adapter is running
+ * @param layout Resource identifier of a layout file that defines the views
+ * for this list item. Unless you override them later, this will
+ * define both the item views and the drop down views.
+ * @param c The cursor from which to get the data.
+ * @param flags Flags used to determine the behavior of the adapter,
+ * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
+ */
+ public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c, int flags) {
+ super(context, c, flags);
+ mLayout = mDropDownLayout = layout;
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ /**
+ * Inflates view(s) from the specified XML file.
+ *
+ * @see android.widget.CursorAdapter#newView(android.content.Context,
+ * android.database.Cursor, ViewGroup)
+ */
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return mInflater.inflate(mLayout, parent, false);
+ }
+
+ @Override
+ public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
+ return mInflater.inflate(mDropDownLayout, parent, false);
+ }
+
+ /**
+ * <p>Sets the layout resource of the item views.</p>
+ *
+ * @param layout the layout resources used to create item views
+ */
+ public void setViewResource(int layout) {
+ mLayout = layout;
+ }
+
+ /**
+ * <p>Sets the layout resource of the drop down views.</p>
+ *
+ * @param dropDownLayout the layout resources used to create drop down views
+ */
+ public void setDropDownViewResource(int dropDownLayout) {
+ mDropDownLayout = dropDownLayout;
+ }
+}
diff --git a/ring-android/src/cx/ring/views/dragsortlv/SimpleDragSortCursorAdapter.java b/ring-android/src/cx/ring/views/dragsortlv/SimpleDragSortCursorAdapter.java
new file mode 100644
index 0000000..df38a32
--- /dev/null
+++ b/ring-android/src/cx/ring/views/dragsortlv/SimpleDragSortCursorAdapter.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2006 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 cx.ring.views.dragsortlv;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.ImageView;
+
+// taken from sdk/sources/android-16/android/widget/SimpleCursorAdapter.java
+
+/**
+ * An easy adapter to map columns from a cursor to TextViews or ImageViews
+ * defined in an XML file. You can specify which columns you want, which
+ * views you want to display the columns, and the XML file that defines
+ * the appearance of these views.
+ *
+ * Binding occurs in two phases. First, if a
+ * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
+ * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
+ * is invoked. If the returned value is true, binding has occured. If the
+ * returned value is false and the view to bind is a TextView,
+ * {@link #setViewText(TextView, String)} is invoked. If the returned value
+ * is false and the view to bind is an ImageView,
+ * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
+ * binding can be found, an {@link IllegalStateException} is thrown.
+ *
+ * If this adapter is used with filtering, for instance in an
+ * {@link android.widget.AutoCompleteTextView}, you can use the
+ * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} and the
+ * {@link android.widget.FilterQueryProvider} interfaces
+ * to get control over the filtering process. You can refer to
+ * {@link #convertToString(android.database.Cursor)} and
+ * {@link #runQueryOnBackgroundThread(CharSequence)} for more information.
+ */
+public class SimpleDragSortCursorAdapter extends ResourceDragSortCursorAdapter {
+ /**
+ * A list of columns containing the data to bind to the UI.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected int[] mFrom;
+ /**
+ * A list of View ids representing the views to which the data must be bound.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected int[] mTo;
+
+ private int mStringConversionColumn = -1;
+ private CursorToStringConverter mCursorToStringConverter;
+ private ViewBinder mViewBinder;
+
+ String[] mOriginalFrom;
+
+ /**
+ * Constructor the enables auto-requery.
+ *
+ * @deprecated This option is discouraged, as it results in Cursor queries
+ * being performed on the application's UI thread and thus can cause poor
+ * responsiveness or even Application Not Responding errors. As an alternative,
+ * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
+ */
+ @Deprecated
+ public SimpleDragSortCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
+ super(context, layout, c);
+ mTo = to;
+ mOriginalFrom = from;
+ findColumns(c, from);
+ }
+
+ /**
+ * Standard constructor.
+ *
+ * @param context The context where the ListView associated with this
+ * SimpleListItemFactory is running
+ * @param layout resource identifier of a layout file that defines the views
+ * for this list item. The layout file should include at least
+ * those named views defined in "to"
+ * @param c The database cursor. Can be null if the cursor is not available yet.
+ * @param from A list of column names representing the data to bind to the UI. Can be null
+ * if the cursor is not available yet.
+ * @param to The views that should display column in the "from" parameter.
+ * These should all be TextViews. The first N views in this list
+ * are given the values of the first N columns in the from
+ * parameter. Can be null if the cursor is not available yet.
+ * @param flags Flags used to determine the behavior of the adapter,
+ * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
+ */
+ public SimpleDragSortCursorAdapter(Context context, int layout,
+ Cursor c, String[] from, int[] to, int flags) {
+ super(context, layout, c, flags);
+ mTo = to;
+ mOriginalFrom = from;
+ findColumns(c, from);
+ }
+
+ /**
+ * Binds all of the field names passed into the "to" parameter of the
+ * constructor with their corresponding cursor columns as specified in the
+ * "from" parameter.
+ *
+ * Binding occurs in two phases. First, if a
+ * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
+ * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
+ * is invoked. If the returned value is true, binding has occured. If the
+ * returned value is false and the view to bind is a TextView,
+ * {@link #setViewText(TextView, String)} is invoked. If the returned value is
+ * false and the view to bind is an ImageView,
+ * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
+ * binding can be found, an {@link IllegalStateException} is thrown.
+ *
+ * @throws IllegalStateException if binding cannot occur
+ *
+ * @see android.widget.CursorAdapter#bindView(android.view.View,
+ * android.content.Context, android.database.Cursor)
+ * @see #getViewBinder()
+ * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
+ * @see #setViewImage(ImageView, String)
+ * @see #setViewText(TextView, String)
+ */
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ final ViewBinder binder = mViewBinder;
+ final int count = mTo.length;
+ final int[] from = mFrom;
+ final int[] to = mTo;
+
+ for (int i = 0; i < count; i++) {
+ final View v = view.findViewById(to[i]);
+ if (v != null) {
+ boolean bound = false;
+ if (binder != null) {
+ bound = binder.setViewValue(v, cursor, from[i]);
+ }
+
+ if (!bound) {
+ String text = cursor.getString(from[i]);
+ if (text == null) {
+ text = "";
+ }
+
+ if (v instanceof TextView) {
+ setViewText((TextView) v, text);
+ } else if (v instanceof ImageView) {
+ setViewImage((ImageView) v, text);
+ } else {
+ throw new IllegalStateException(v.getClass().getName() + " is not a " +
+ " view that can be bounds by this SimpleCursorAdapter");
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link ViewBinder} used to bind data to views.
+ *
+ * @return a ViewBinder or null if the binder does not exist
+ *
+ * @see #bindView(android.view.View, android.content.Context, android.database.Cursor)
+ * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
+ */
+ public ViewBinder getViewBinder() {
+ return mViewBinder;
+ }
+
+ /**
+ * Sets the binder used to bind data to views.
+ *
+ * @param viewBinder the binder used to bind data to views, can be null to
+ * remove the existing binder
+ *
+ * @see #bindView(android.view.View, android.content.Context, android.database.Cursor)
+ * @see #getViewBinder()
+ */
+ public void setViewBinder(ViewBinder viewBinder) {
+ mViewBinder = viewBinder;
+ }
+
+ /**
+ * Called by bindView() to set the image for an ImageView but only if
+ * there is no existing ViewBinder or if the existing ViewBinder cannot
+ * handle binding to an ImageView.
+ *
+ * By default, the value will be treated as an image resource. If the
+ * value cannot be used as an image resource, the value is used as an
+ * image Uri.
+ *
+ * Intended to be overridden by Adapters that need to filter strings
+ * retrieved from the database.
+ *
+ * @param v ImageView to receive an image
+ * @param value the value retrieved from the cursor
+ */
+ public void setViewImage(ImageView v, String value) {
+ try {
+ v.setImageResource(Integer.parseInt(value));
+ } catch (NumberFormatException nfe) {
+ v.setImageURI(Uri.parse(value));
+ }
+ }
+
+ /**
+ * Called by bindView() to set the text for a TextView but only if
+ * there is no existing ViewBinder or if the existing ViewBinder cannot
+ * handle binding to a TextView.
+ *
+ * Intended to be overridden by Adapters that need to filter strings
+ * retrieved from the database.
+ *
+ * @param v TextView to receive text
+ * @param text the text to be set for the TextView
+ */
+ public void setViewText(TextView v, String text) {
+ v.setText(text);
+ }
+
+ /**
+ * Return the index of the column used to get a String representation
+ * of the Cursor.
+ *
+ * @return a valid index in the current Cursor or -1
+ *
+ * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+ * @see #setStringConversionColumn(int)
+ * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
+ * @see #getCursorToStringConverter()
+ */
+ public int getStringConversionColumn() {
+ return mStringConversionColumn;
+ }
+
+ /**
+ * Defines the index of the column in the Cursor used to get a String
+ * representation of that Cursor. The column is used to convert the
+ * Cursor to a String only when the current CursorToStringConverter
+ * is null.
+ *
+ * @param stringConversionColumn a valid index in the current Cursor or -1 to use the default
+ * conversion mechanism
+ *
+ * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+ * @see #getStringConversionColumn()
+ * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
+ * @see #getCursorToStringConverter()
+ */
+ public void setStringConversionColumn(int stringConversionColumn) {
+ mStringConversionColumn = stringConversionColumn;
+ }
+
+ /**
+ * Returns the converter used to convert the filtering Cursor
+ * into a String.
+ *
+ * @return null if the converter does not exist or an instance of
+ * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter}
+ *
+ * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
+ * @see #getStringConversionColumn()
+ * @see #setStringConversionColumn(int)
+ * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+ */
+ public CursorToStringConverter getCursorToStringConverter() {
+ return mCursorToStringConverter;
+ }
+
+ /**
+ * Sets the converter used to convert the filtering Cursor
+ * into a String.
+ *
+ * @param cursorToStringConverter the Cursor to String converter, or
+ * null to remove the converter
+ *
+ * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
+ * @see #getStringConversionColumn()
+ * @see #setStringConversionColumn(int)
+ * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+ */
+ public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) {
+ mCursorToStringConverter = cursorToStringConverter;
+ }
+
+ /**
+ * Returns a CharSequence representation of the specified Cursor as defined
+ * by the current CursorToStringConverter. If no CursorToStringConverter
+ * has been set, the String conversion column is used instead. If the
+ * conversion column is -1, the returned String is empty if the cursor
+ * is null or Cursor.toString().
+ *
+ * @param cursor the Cursor to convert to a CharSequence
+ *
+ * @return a non-null CharSequence representing the cursor
+ */
+ @Override
+ public CharSequence convertToString(Cursor cursor) {
+ if (mCursorToStringConverter != null) {
+ return mCursorToStringConverter.convertToString(cursor);
+ } else if (mStringConversionColumn > -1) {
+ return cursor.getString(mStringConversionColumn);
+ }
+
+ return super.convertToString(cursor);
+ }
+
+ /**
+ * Create a map from an array of strings to an array of column-id integers in cursor c.
+ * If c is null, the array will be discarded.
+ *
+ * @param c the cursor to find the columns from
+ * @param from the Strings naming the columns of interest
+ */
+ private void findColumns(Cursor c, String[] from) {
+ if (c != null) {
+ int i;
+ int count = from.length;
+ if (mFrom == null || mFrom.length != count) {
+ mFrom = new int[count];
+ }
+ for (i = 0; i < count; i++) {
+ mFrom[i] = c.getColumnIndexOrThrow(from[i]);
+ }
+ } else {
+ mFrom = null;
+ }
+ }
+
+ @Override
+ public Cursor swapCursor(Cursor c) {
+ // super.swapCursor() will notify observers before we have
+ // a valid mapping, make sure we have a mapping before this
+ // happens
+ findColumns(c, mOriginalFrom);
+ return super.swapCursor(c);
+ }
+
+ /**
+ * Change the cursor and change the column-to-view mappings at the same time.
+ *
+ * @param c The database cursor. Can be null if the cursor is not available yet.
+ * @param from A list of column names representing the data to bind to the UI. Can be null
+ * if the cursor is not available yet.
+ * @param to The views that should display column in the "from" parameter.
+ * These should all be TextViews. The first N views in this list
+ * are given the values of the first N columns in the from
+ * parameter. Can be null if the cursor is not available yet.
+ */
+ public void changeCursorAndColumns(Cursor c, String[] from, int[] to) {
+ mOriginalFrom = from;
+ mTo = to;
+ // super.changeCursor() will notify observers before we have
+ // a valid mapping, make sure we have a mapping before this
+ // happens
+ findColumns(c, mOriginalFrom);
+ super.changeCursor(c);
+ }
+
+ /**
+ * This class can be used by external clients of SimpleCursorAdapter
+ * to bind values fom the Cursor to views.
+ *
+ * You should use this class to bind values from the Cursor to views
+ * that are not directly supported by SimpleCursorAdapter or to
+ * change the way binding occurs for views supported by
+ * SimpleCursorAdapter.
+ *
+ * @see SimpleCursorAdapter#bindView(android.view.View, android.content.Context, android.database.Cursor)
+ * @see SimpleCursorAdapter#setViewImage(ImageView, String)
+ * @see SimpleCursorAdapter#setViewText(TextView, String)
+ */
+ public static interface ViewBinder {
+ /**
+ * Binds the Cursor column defined by the specified index to the specified view.
+ *
+ * When binding is handled by this ViewBinder, this method must return true.
+ * If this method returns false, SimpleCursorAdapter will attempts to handle
+ * the binding on its own.
+ *
+ * @param view the view to bind the data to
+ * @param cursor the cursor to get the data from
+ * @param columnIndex the column at which the data can be found in the cursor
+ *
+ * @return true if the data was bound to the view, false otherwise
+ */
+ boolean setViewValue(View view, Cursor cursor, int columnIndex);
+ }
+
+ /**
+ * This class can be used by external clients of SimpleCursorAdapter
+ * to define how the Cursor should be converted to a String.
+ *
+ * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+ */
+ public static interface CursorToStringConverter {
+ /**
+ * Returns a CharSequence representing the specified Cursor.
+ *
+ * @param cursor the cursor for which a CharSequence representation
+ * is requested
+ *
+ * @return a non-null CharSequence representing the cursor
+ */
+ CharSequence convertToString(Cursor cursor);
+ }
+
+}
diff --git a/ring-android/src/cx/ring/views/dragsortlv/SimpleFloatViewManager.java b/ring-android/src/cx/ring/views/dragsortlv/SimpleFloatViewManager.java
new file mode 100644
index 0000000..5059f59
--- /dev/null
+++ b/ring-android/src/cx/ring/views/dragsortlv/SimpleFloatViewManager.java
@@ -0,0 +1,88 @@
+package cx.ring.views.dragsortlv;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ListView;
+
+/**
+ * Simple implementation of the FloatViewManager class. Uses list
+ * items as they appear in the ListView to create the floating View.
+ */
+public class SimpleFloatViewManager implements DragSortListView.FloatViewManager {
+
+ private Bitmap mFloatBitmap;
+
+ private ImageView mImageView;
+
+ private int mFloatBGColor = Color.BLACK;
+
+ private ListView mListView;
+
+ public SimpleFloatViewManager(ListView lv) {
+ mListView = lv;
+ }
+
+ public void setBackgroundColor(int color) {
+ mFloatBGColor = color;
+ }
+
+ /**
+ * This simple implementation creates a Bitmap copy of the
+ * list item currently shown at ListView <code>position</code>.
+ */
+ @Override
+ public View onCreateFloatView(int position) {
+ // Guaranteed that this will not be null? I think so. Nope, got
+ // a NullPointerException once...
+ View v = mListView.getChildAt(position + mListView.getHeaderViewsCount() - mListView.getFirstVisiblePosition());
+
+ if (v == null) {
+ return null;
+ }
+
+ v.setPressed(false);
+
+ // Create a copy of the drawing cache so that it does not get
+ // recycled by the framework when the list tries to clean up memory
+ //v.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
+ v.setDrawingCacheEnabled(true);
+ mFloatBitmap = Bitmap.createBitmap(v.getDrawingCache());
+ v.setDrawingCacheEnabled(false);
+
+ if (mImageView == null) {
+ mImageView = new ImageView(mListView.getContext());
+ }
+ mImageView.setBackgroundColor(mFloatBGColor);
+ mImageView.setPadding(0, 0, 0, 0);
+ mImageView.setImageBitmap(mFloatBitmap);
+ mImageView.setLayoutParams(new ViewGroup.LayoutParams(v.getWidth(), v.getHeight()));
+
+ return mImageView;
+ }
+
+ /**
+ * This does nothing
+ */
+ @Override
+ public void onDragFloatView(View floatView, Point position, Point touch) {
+ // do nothing
+ }
+
+ /**
+ * Removes the Bitmap from the ImageView created in
+ * onCreateFloatView() and tells the system to recycle it.
+ */
+ @Override
+ public void onDestroyFloatView(View floatView) {
+ ((ImageView) floatView).setImageDrawable(null);
+
+ mFloatBitmap.recycle();
+ mFloatBitmap = null;
+ }
+
+}
+
diff --git a/ring-android/src/cx/ring/views/stickylistheaders/AdapterWrapper.java b/ring-android/src/cx/ring/views/stickylistheaders/AdapterWrapper.java
new file mode 100644
index 0000000..22b02a8
--- /dev/null
+++ b/ring-android/src/cx/ring/views/stickylistheaders/AdapterWrapper.java
@@ -0,0 +1,225 @@
+package cx.ring.views.stickylistheaders;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Checkable;
+import android.widget.ListAdapter;
+
+/**
+ * A {@link ListAdapter} which wraps a {@link StickyListHeadersAdapter} and
+ * automatically handles wrapping the result of
+ * {@link StickyListHeadersAdapter#getView(int, android.view.View, android.view.ViewGroup)}
+ * and
+ * {@link StickyListHeadersAdapter#getHeaderView(int, android.view.View, android.view.ViewGroup)}
+ * appropriately.
+ *
+ * @author Jake Wharton (jakewharton@gmail.com)
+ */
+class AdapterWrapper extends BaseAdapter implements StickyListHeadersAdapter {
+
+ interface OnHeaderClickListener {
+ public void onHeaderClick(View header, int itemPosition, long headerId);
+ }
+
+ final StickyListHeadersAdapter mDelegate;
+ private final List<View> mHeaderCache = new LinkedList<View>();
+ private final Context mContext;
+ private Drawable mDivider;
+ private int mDividerHeight;
+ private OnHeaderClickListener mOnHeaderClickListener;
+ private DataSetObserver mDataSetObserver = new DataSetObserver() {
+
+ @Override
+ public void onInvalidated() {
+ mHeaderCache.clear();
+ AdapterWrapper.super.notifyDataSetInvalidated();
+ }
+
+ @Override
+ public void onChanged() {
+ AdapterWrapper.super.notifyDataSetChanged();
+ }
+ };
+
+ AdapterWrapper(Context context,
+ StickyListHeadersAdapter delegate) {
+ this.mContext = context;
+ this.mDelegate = delegate;
+ delegate.registerDataSetObserver(mDataSetObserver);
+ }
+
+ void setDivider(Drawable divider, int dividerHeight) {
+ this.mDivider = divider;
+ this.mDividerHeight = dividerHeight;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return mDelegate.areAllItemsEnabled();
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return mDelegate.isEnabled(position);
+ }
+
+ @Override
+ public int getCount() {
+ return mDelegate.getCount();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mDelegate.getItem(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mDelegate.getItemId(position);
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return mDelegate.hasStableIds();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return mDelegate.getItemViewType(position);
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return mDelegate.getViewTypeCount();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return mDelegate.isEmpty();
+ }
+
+ /**
+ * Will recycle header from {@link WrapperView} if it exists
+ */
+ private void recycleHeaderIfExists(WrapperView wv) {
+ View header = wv.mHeader;
+ if (header != null) {
+ // reset the headers visibility when adding it to the cache
+ header.setVisibility(View.VISIBLE);
+ mHeaderCache.add(header);
+ }
+ }
+
+ /**
+ * Get a header view. This optionally pulls a header from the supplied
+ * {@link WrapperView} and will also recycle the divider if it exists.
+ */
+ private View configureHeader(WrapperView wv, final int position) {
+ View header = wv.mHeader == null ? popHeader() : wv.mHeader;
+ header = mDelegate.getHeaderView(position, header, wv);
+ if (header == null) {
+ throw new NullPointerException("Header view must not be null.");
+ }
+ //if the header isn't clickable, the listselector will be drawn on top of the header
+ header.setClickable(true);
+ header.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if(mOnHeaderClickListener != null){
+ long headerId = mDelegate.getHeaderId(position);
+ mOnHeaderClickListener.onHeaderClick(v, position, headerId);
+ }
+ }
+ });
+ return header;
+ }
+
+ private View popHeader() {
+ if(mHeaderCache.size() > 0) {
+ return mHeaderCache.remove(0);
+ }
+ return null;
+ }
+
+ /** Returns {@code true} if the previous position has the same header ID. */
+ private boolean previousPositionHasSameHeader(int position) {
+ return position != 0
+ && mDelegate.getHeaderId(position) == mDelegate
+ .getHeaderId(position - 1);
+ }
+
+ @Override
+ public WrapperView getView(int position, View convertView, ViewGroup parent) {
+ WrapperView wv = (convertView == null) ? new WrapperView(mContext) : (WrapperView) convertView;
+ View item = mDelegate.getView(position, wv.mItem, parent);
+ View header = null;
+ if (previousPositionHasSameHeader(position)) {
+ recycleHeaderIfExists(wv);
+ } else {
+ header = configureHeader(wv, position);
+ }
+ if((item instanceof Checkable) && !(wv instanceof CheckableWrapperView)) {
+ // Need to create Checkable subclass of WrapperView for ListView to work correctly
+ wv = new CheckableWrapperView(mContext);
+ } else if(!(item instanceof Checkable) && (wv instanceof CheckableWrapperView)) {
+ wv = new WrapperView(mContext);
+ }
+ wv.update(item, header, mDivider, mDividerHeight);
+ return wv;
+ }
+
+ public void setOnHeaderClickListener(OnHeaderClickListener onHeaderClickListener){
+ this.mOnHeaderClickListener = onHeaderClickListener;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return mDelegate.equals(o);
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ return ((BaseAdapter) mDelegate).getDropDownView(position, convertView, parent);
+ }
+
+ @Override
+ public int hashCode() {
+ return mDelegate.hashCode();
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ ((BaseAdapter) mDelegate).notifyDataSetChanged();
+ }
+
+ @Override
+ public void notifyDataSetInvalidated() {
+ ((BaseAdapter) mDelegate).notifyDataSetInvalidated();
+ }
+
+ @Override
+ public String toString() {
+ return mDelegate.toString();
+ }
+
+ @Override
+ public View getHeaderView(int position, View convertView, ViewGroup parent) {
+ return mDelegate.getHeaderView(position, convertView, parent);
+ }
+
+ @Override
+ public long getHeaderId(int position) {
+ return mDelegate.getHeaderId(position);
+ }
+
+}
diff --git a/ring-android/src/cx/ring/views/stickylistheaders/ApiLevelTooLowException.java b/ring-android/src/cx/ring/views/stickylistheaders/ApiLevelTooLowException.java
new file mode 100644
index 0000000..075dfb4
--- /dev/null
+++ b/ring-android/src/cx/ring/views/stickylistheaders/ApiLevelTooLowException.java
@@ -0,0 +1,11 @@
+package cx.ring.views.stickylistheaders;
+
+public class ApiLevelTooLowException extends RuntimeException {
+
+ private static final long serialVersionUID = -5480068364264456757L;
+
+ public ApiLevelTooLowException(int versionCode) {
+ super("Requires API level " + versionCode);
+ }
+
+}
diff --git a/ring-android/src/cx/ring/views/stickylistheaders/CheckableWrapperView.java b/ring-android/src/cx/ring/views/stickylistheaders/CheckableWrapperView.java
new file mode 100644
index 0000000..e22204f
--- /dev/null
+++ b/ring-android/src/cx/ring/views/stickylistheaders/CheckableWrapperView.java
@@ -0,0 +1,31 @@
+package cx.ring.views.stickylistheaders;
+
+import android.content.Context;
+import android.widget.Checkable;
+
+/**
+ * A WrapperView that implements the checkable interface
+ *
+ * @author Emil Sjölander
+ */
+class CheckableWrapperView extends WrapperView implements Checkable {
+
+ public CheckableWrapperView(final Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return ((Checkable) mItem).isChecked();
+ }
+
+ @Override
+ public void setChecked(final boolean checked) {
+ ((Checkable) mItem).setChecked(checked);
+ }
+
+ @Override
+ public void toggle() {
+ setChecked(!isChecked());
+ }
+}
diff --git a/ring-android/src/cx/ring/views/stickylistheaders/SectionIndexerAdapterWrapper.java b/ring-android/src/cx/ring/views/stickylistheaders/SectionIndexerAdapterWrapper.java
new file mode 100644
index 0000000..dd823ff
--- /dev/null
+++ b/ring-android/src/cx/ring/views/stickylistheaders/SectionIndexerAdapterWrapper.java
@@ -0,0 +1,32 @@
+package cx.ring.views.stickylistheaders;
+
+import android.content.Context;
+import android.widget.SectionIndexer;
+
+class SectionIndexerAdapterWrapper extends
+ AdapterWrapper implements SectionIndexer {
+
+ final SectionIndexer mSectionIndexerDelegate;
+
+ SectionIndexerAdapterWrapper(Context context,
+ StickyListHeadersAdapter delegate) {
+ super(context, delegate);
+ mSectionIndexerDelegate = (SectionIndexer) delegate;
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ return mSectionIndexerDelegate.getPositionForSection(section);
+ }
+
+ @Override
+ public int getSectionForPosition(int position) {
+ return mSectionIndexerDelegate.getSectionForPosition(position);
+ }
+
+ @Override
+ public Object[] getSections() {
+ return mSectionIndexerDelegate.getSections();
+ }
+
+}
diff --git a/ring-android/src/cx/ring/views/stickylistheaders/StickyListHeadersAdapter.java b/ring-android/src/cx/ring/views/stickylistheaders/StickyListHeadersAdapter.java
new file mode 100644
index 0000000..236338d
--- /dev/null
+++ b/ring-android/src/cx/ring/views/stickylistheaders/StickyListHeadersAdapter.java
@@ -0,0 +1,38 @@
+package cx.ring.views.stickylistheaders;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListAdapter;
+
+public interface StickyListHeadersAdapter extends ListAdapter {
+ /**
+ * Get a View that displays the header data at the specified position in the
+ * set. You can either create a View manually or inflate it from an XML layout
+ * file.
+ *
+ * @param position
+ * The position of the item within the adapter's data set of the item whose
+ * header view we want.
+ * @param convertView
+ * The old view to reuse, if possible. Note: You should check that this view is
+ * non-null and of an appropriate type before using. If it is not possible to
+ * convert this view to display the correct data, this method can create a new
+ * view.
+ * @param parent
+ * The parent that this view will eventually be attached to.
+ * @return
+ * A View corresponding to the data at the specified position.
+ */
+ View getHeaderView(int position, View convertView, ViewGroup parent);
+
+ /**
+ * Get the header id associated with the specified position in the list.
+ *
+ * @param position
+ * The position of the item within the adapter's data set whose header id we
+ * want.
+ * @return
+ * The id of the header at the specified position.
+ */
+ long getHeaderId(int position);
+}
diff --git a/ring-android/src/cx/ring/views/stickylistheaders/StickyListHeadersListView.java b/ring-android/src/cx/ring/views/stickylistheaders/StickyListHeadersListView.java
new file mode 100644
index 0000000..72780e9
--- /dev/null
+++ b/ring-android/src/cx/ring/views/stickylistheaders/StickyListHeadersListView.java
@@ -0,0 +1,993 @@
+package cx.ring.views.stickylistheaders;
+
+import cx.ring.R;
+import cx.ring.adapters.ContactsAdapter;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.SparseBooleanArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.FrameLayout;
+import android.widget.ListView;
+import android.widget.SectionIndexer;
+
+/**
+ * Even though this is a FrameLayout subclass we it is called a ListView. This
+ * is because of 2 reasons. 1. It acts like as ListView 2. It used to be a
+ * ListView subclass and i did not was to change to name causing compatibility
+ * errors.
+ *
+ * @author Emil Sjölander
+ */
+public class StickyListHeadersListView extends FrameLayout {
+
+ public interface OnHeaderClickListener {
+ public void onHeaderClick(StickyListHeadersListView l, View header,
+ int itemPosition, long headerId, boolean currentlySticky);
+ }
+
+ /* --- Children --- */
+ private WrapperViewList mList;
+ private View mHeader;
+
+ /* --- Header state --- */
+ private Long mHeaderId;
+ // used to not have to call getHeaderId() all the time
+ private Integer mHeaderPosition;
+ private Integer mHeaderOffset;
+
+ /* --- Delegates --- */
+ private OnScrollListener mOnScrollListenerDelegate;
+
+ /* --- Settings --- */
+ private boolean mAreHeadersSticky = true;
+ private boolean mClippingToPadding = true;
+ private boolean mIsDrawingListUnderStickyHeader = true;
+ private int mPaddingLeft = 0;
+ private int mPaddingTop = 0;
+ private int mPaddingRight = 0;
+ private int mPaddingBottom = 0;
+
+ /* --- Other --- */
+ private AdapterWrapper mAdapter;
+ private OnHeaderClickListener mOnHeaderClickListener;
+ private Drawable mDivider;
+ private int mDividerHeight;
+ private AdapterWrapperDataSetObserver mDataSetObserver;
+
+ public StickyListHeadersListView(Context context) {
+ this(context, null);
+ }
+
+ public StickyListHeadersListView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public StickyListHeadersListView(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+
+ // Initialize the list
+ mList = new WrapperViewList(context, attrs);
+ mDivider = mList.getDivider();
+ mDividerHeight = mList.getDividerHeight();
+
+ // null out divider, dividers are handled by adapter so they look good
+ // with headers
+ mList.setDivider(null);
+ mList.setDividerHeight(0);
+
+ mList.setLifeCycleListener(new WrapperViewListLifeCycleListener());
+ mList.setOnScrollListener(new WrapperListScrollListener());
+ addView(mList);
+
+ if (attrs != null) {
+ TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
+ R.styleable.StickyListHeadersListView, 0, 0);
+
+ try {
+ // Android attributes
+ if (a.hasValue(R.styleable.StickyListHeadersListView_android_padding)) {
+ int padding = a
+ .getDimensionPixelSize(
+ R.styleable.StickyListHeadersListView_android_padding,
+ 0);
+ mPaddingLeft = padding;
+ mPaddingTop = padding;
+ mPaddingRight = padding;
+ mPaddingBottom = padding;
+ } else {
+ mPaddingLeft = a
+ .getDimensionPixelSize(
+ R.styleable.StickyListHeadersListView_android_paddingLeft,
+ 0);
+ mPaddingTop = a
+ .getDimensionPixelSize(
+ R.styleable.StickyListHeadersListView_android_paddingTop,
+ 0);
+ mPaddingRight = a
+ .getDimensionPixelSize(
+ R.styleable.StickyListHeadersListView_android_paddingRight,
+ 0);
+ mPaddingBottom = a
+ .getDimensionPixelSize(
+ R.styleable.StickyListHeadersListView_android_paddingBottom,
+ 0);
+ }
+ setPadding(mPaddingLeft, mPaddingTop, mPaddingRight,
+ mPaddingBottom);
+
+ // Set clip to padding on the list and reset value to default on
+ // wrapper
+ mClippingToPadding = a
+ .getBoolean(
+ R.styleable.StickyListHeadersListView_android_clipToPadding,
+ true);
+ super.setClipToPadding(true);
+ mList.setClipToPadding(mClippingToPadding);
+
+ // ListView attributes
+ mList.setFadingEdgeLength(a
+ .getDimensionPixelSize(
+ R.styleable.StickyListHeadersListView_android_fadingEdgeLength,
+ mList.getVerticalFadingEdgeLength()));
+ final int fadingEdge = a
+ .getInt(R.styleable.StickyListHeadersListView_android_requiresFadingEdge,
+ 0);
+ if (fadingEdge == 0x00001000) {
+ mList.setVerticalFadingEdgeEnabled(false);
+ mList.setHorizontalFadingEdgeEnabled(true);
+ } else if (fadingEdge == 0x00002000) {
+ mList.setVerticalFadingEdgeEnabled(true);
+ mList.setHorizontalFadingEdgeEnabled(false);
+ } else {
+ mList.setVerticalFadingEdgeEnabled(false);
+ mList.setHorizontalFadingEdgeEnabled(false);
+ }
+ mList.setCacheColorHint(a
+ .getColor(
+ R.styleable.StickyListHeadersListView_android_cacheColorHint,
+ mList.getCacheColorHint()));
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ mList.setChoiceMode(a
+ .getInt(R.styleable.StickyListHeadersListView_android_choiceMode,
+ mList.getChoiceMode()));
+ }
+ mList.setDrawSelectorOnTop(a
+ .getBoolean(
+ R.styleable.StickyListHeadersListView_android_drawSelectorOnTop,
+ false));
+ mList.setFastScrollEnabled(a
+ .getBoolean(
+ R.styleable.StickyListHeadersListView_android_fastScrollEnabled,
+ mList.isFastScrollEnabled()));
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ mList.setFastScrollAlwaysVisible(a
+ .getBoolean(
+ R.styleable.StickyListHeadersListView_android_fastScrollAlwaysVisible,
+ mList.isFastScrollAlwaysVisible()));
+ }
+ mList.setScrollBarStyle(a
+ .getInt(R.styleable.StickyListHeadersListView_android_scrollbarStyle,
+ 0));
+ final Drawable selector = a
+ .getDrawable(R.styleable.StickyListHeadersListView_android_listSelector);
+ if (selector != null) {
+ mList.setSelector(selector);
+ }
+ mList.setScrollingCacheEnabled(a
+ .getBoolean(
+ R.styleable.StickyListHeadersListView_android_scrollingCache,
+ mList.isScrollingCacheEnabled()));
+ final Drawable divider = a
+ .getDrawable(R.styleable.StickyListHeadersListView_android_divider);
+ if (divider != null) {
+ mDivider = divider;
+ }
+ mDividerHeight = a
+ .getDimensionPixelSize(
+ R.styleable.StickyListHeadersListView_android_dividerHeight,
+ mDividerHeight);
+
+ // StickyListHeaders attributes
+ mAreHeadersSticky = a.getBoolean(
+ R.styleable.StickyListHeadersListView_hasStickyHeaders,
+ true);
+ mIsDrawingListUnderStickyHeader = a
+ .getBoolean(
+ R.styleable.StickyListHeadersListView_isDrawingListUnderStickyHeader,
+ true);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ mList.setVerticalScrollBarEnabled(isVerticalScrollBarEnabled());
+ mList.setHorizontalScrollBarEnabled(isHorizontalScrollBarEnabled());
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ measureHeader(mHeader);
+ }
+
+ private void ensureHeaderHasCorrectLayoutParams(View header) {
+ ViewGroup.LayoutParams lp = header.getLayoutParams();
+ if (lp == null) {
+ lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ } else if (lp.height == LayoutParams.MATCH_PARENT) {
+ lp.height = LayoutParams.WRAP_CONTENT;
+ }
+ header.setLayoutParams(lp);
+ }
+
+ private void measureHeader(View header) {
+ if (header != null) {
+ final int width = getMeasuredWidth() - mPaddingLeft - mPaddingRight;
+ final int parentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ width, MeasureSpec.EXACTLY);
+ final int parentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0,
+ MeasureSpec.UNSPECIFIED);
+ measureChild(header, parentWidthMeasureSpec,
+ parentHeightMeasureSpec);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right,
+ int bottom) {
+ mList.layout(0, 0, mList.getMeasuredWidth(), getHeight());
+ if (mHeader != null) {
+ MarginLayoutParams lp = (MarginLayoutParams) mHeader
+ .getLayoutParams();
+ int headerTop = lp.topMargin
+ + (mClippingToPadding ? mPaddingTop : 0);
+ // The left parameter must for some reason be set to 0.
+ // I think it should be set to mPaddingLeft but apparently not
+ mHeader.layout(mPaddingLeft, headerTop, mHeader.getMeasuredWidth()
+ + mPaddingLeft, headerTop + mHeader.getMeasuredHeight());
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ // Only draw the list here.
+ // The header should be drawn right after the lists children are drawn.
+ // This is done so that the header is above the list items
+ // but below the list decorators (scroll bars etc).
+ drawChild(canvas, mList, 0);
+ }
+
+ // Reset values tied the header. also remove header form layout
+ // This is called in response to the data set or the adapter changing
+ private void clearHeader() {
+ if (mHeader != null) {
+ removeView(mHeader);
+ mHeader = null;
+ mHeaderId = null;
+ mHeaderPosition = null;
+ mHeaderOffset = null;
+
+ // reset the top clipping length
+ mList.setTopClippingLength(0);
+ updateHeaderVisibilities();
+ }
+ }
+
+ private void updateOrClearHeader(int firstVisiblePosition) {
+ final int adapterCount = mAdapter == null ? 0 : mAdapter.getCount();
+ if (adapterCount == 0 || !mAreHeadersSticky) {
+ return;
+ }
+
+ final int headerViewCount = mList.getHeaderViewsCount();
+ final int realFirstVisibleItem = firstVisiblePosition - headerViewCount;
+
+ // It is not a mistake to call getFirstVisiblePosition() here.
+ // Most of the time getFixedFirstVisibleItem() should be called
+ // but that does not work great together with getChildAt()
+ final boolean doesListHaveChildren = mList.getChildCount() != 0;
+ final boolean isFirstViewBelowTop = doesListHaveChildren && mList.getFirstVisiblePosition() == 0
+ && mList.getChildAt(0).getTop() > 0;
+ final boolean isFirstVisibleItemOutsideAdapterRange = realFirstVisibleItem > adapterCount - 1
+ || realFirstVisibleItem < 0;
+ if (!doesListHaveChildren || isFirstVisibleItemOutsideAdapterRange
+ || isFirstViewBelowTop) {
+ clearHeader();
+ return;
+ }
+
+ updateHeader(realFirstVisibleItem);
+ }
+
+ private void updateHeader(int firstVisiblePosition) {
+
+ // check if there is a new header should be sticky
+ if (mHeaderPosition == null || mHeaderPosition != firstVisiblePosition) {
+ mHeaderPosition = firstVisiblePosition;
+ final long headerId = mAdapter.getHeaderId(firstVisiblePosition);
+ if (mHeaderId == null || mHeaderId != headerId) {
+ mHeaderId = headerId;
+ final View header = mAdapter.getHeaderView(mHeaderPosition,
+ mHeader, this);
+ if (mHeader != header) {
+ if (header == null) {
+ throw new NullPointerException("header may not be null");
+ }
+ swapHeader(header);
+ }
+
+ ensureHeaderHasCorrectLayoutParams(mHeader);
+ measureHeader(mHeader);
+
+ // Reset mHeaderOffset to null ensuring
+ // that it will be set on the header and
+ // not skipped for performance reasons.
+ mHeaderOffset = null;
+ }
+ }
+
+ int headerOffset = 0;
+
+ // Calculate new header offset
+ // Skip looking at the first view. it never matters because it always
+ // results in a headerOffset = 0
+ int headerBottom = mHeader.getMeasuredHeight()
+ + (mClippingToPadding ? mPaddingTop : 0);
+ for (int i = 0; i < mList.getChildCount(); i++) {
+ final View child = mList.getChildAt(i);
+ final boolean doesChildHaveHeader = child instanceof WrapperView
+ && ((WrapperView) child).hasHeader();
+ final boolean isChildFooter = mList.containsFooterView(child);
+ if (child.getTop() >= (mClippingToPadding ? mPaddingTop : 0)
+ && (doesChildHaveHeader || isChildFooter)) {
+ headerOffset = Math.min(child.getTop() - headerBottom, 0);
+ break;
+ }
+ }
+
+ setHeaderOffet(headerOffset);
+
+ if (!mIsDrawingListUnderStickyHeader) {
+ mList.setTopClippingLength(mHeader.getMeasuredHeight()
+ + mHeaderOffset);
+ }
+
+ updateHeaderVisibilities();
+ }
+
+ private void swapHeader(View newHeader) {
+ if (mHeader != null) {
+ removeView(mHeader);
+ }
+ mHeader = newHeader;
+ addView(mHeader);
+ mHeader.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (mOnHeaderClickListener != null) {
+ mOnHeaderClickListener.onHeaderClick(
+ StickyListHeadersListView.this, mHeader,
+ mHeaderPosition, mHeaderId, true);
+ }
+ }
+
+ });
+ }
+
+ // hides the headers in the list under the sticky header.
+ // Makes sure the other ones are showing
+ private void updateHeaderVisibilities() {
+ int top;
+ if (mHeader != null) {
+ top = mHeader.getMeasuredHeight()
+ + (mHeaderOffset != null ? mHeaderOffset : 0);
+ } else {
+ top = mClippingToPadding ? mPaddingTop : 0;
+ }
+ int childCount = mList.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = mList.getChildAt(i);
+ if (child instanceof WrapperView) {
+ WrapperView wrapperViewChild = (WrapperView) child;
+ if (wrapperViewChild.hasHeader()) {
+ View childHeader = wrapperViewChild.mHeader;
+ if (wrapperViewChild.getTop() < top) {
+ if (childHeader.getVisibility() != View.INVISIBLE) {
+ childHeader.setVisibility(View.INVISIBLE);
+ }
+ } else {
+ if (childHeader.getVisibility() != View.VISIBLE) {
+ childHeader.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Wrapper around setting the header offset in different ways depending on
+ // the API version
+ @SuppressLint("NewApi")
+ private void setHeaderOffet(int offset) {
+ if (mHeaderOffset == null || mHeaderOffset != offset) {
+ mHeaderOffset = offset;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ mHeader.setTranslationY(mHeaderOffset);
+ } else {
+ MarginLayoutParams params = (MarginLayoutParams) mHeader
+ .getLayoutParams();
+ params.topMargin = mHeaderOffset;
+ mHeader.setLayoutParams(params);
+ }
+ }
+ }
+
+ private class AdapterWrapperDataSetObserver extends DataSetObserver {
+
+ @Override
+ public void onChanged() {
+ clearHeader();
+ }
+
+ @Override
+ public void onInvalidated() {
+ clearHeader();
+ }
+
+ }
+
+ private class WrapperListScrollListener implements OnScrollListener {
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem,
+ int visibleItemCount, int totalItemCount) {
+ if (mOnScrollListenerDelegate != null) {
+ mOnScrollListenerDelegate.onScroll(view, firstVisibleItem,
+ visibleItemCount, totalItemCount);
+ }
+ updateOrClearHeader(mList.getFixedFirstVisibleItem());
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (mOnScrollListenerDelegate != null) {
+ mOnScrollListenerDelegate.onScrollStateChanged(view,
+ scrollState);
+ }
+ }
+
+ }
+
+ private class WrapperViewListLifeCycleListener implements WrapperViewList.LifeCycleListener {
+
+ @Override
+ public void onDispatchDrawOccurred(Canvas canvas) {
+ // onScroll is not called often at all before froyo
+ // therefor we need to update the header here as well.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
+ updateOrClearHeader(mList.getFixedFirstVisibleItem());
+ }
+ if (mHeader != null) {
+ if (mClippingToPadding) {
+ canvas.save();
+ canvas.clipRect(0, mPaddingTop, getRight(), getBottom());
+ drawChild(canvas, mHeader, 0);
+ canvas.restore();
+ } else {
+ drawChild(canvas, mHeader, 0);
+ }
+ }
+ }
+
+ }
+
+ private class AdapterWrapperHeaderClickHandler implements
+ AdapterWrapper.OnHeaderClickListener {
+
+ @Override
+ public void onHeaderClick(View header, int itemPosition, long headerId) {
+ mOnHeaderClickListener.onHeaderClick(
+ StickyListHeadersListView.this, header, itemPosition,
+ headerId, false);
+ }
+
+ }
+
+ private boolean isStartOfSection(int position) {
+ return position == 0
+ || mAdapter.getHeaderId(position) != mAdapter
+ .getHeaderId(position - 1);
+ }
+
+ private int getHeaderOverlap(int position) {
+ boolean isStartOfSection = isStartOfSection(position);
+ if (!isStartOfSection) {
+ View header = mAdapter.getHeaderView(position, null, mList);
+ if (header == null) {
+ throw new NullPointerException("header may not be null");
+ }
+ ensureHeaderHasCorrectLayoutParams(header);
+ measureHeader(header);
+ return header.getMeasuredHeight();
+ }
+ return 0;
+ }
+
+ /* ---------- StickyListHeaders specific API ---------- */
+
+ public void setAreHeadersSticky(boolean areHeadersSticky) {
+ mAreHeadersSticky = areHeadersSticky;
+ if (!areHeadersSticky) {
+ clearHeader();
+ } else {
+ updateOrClearHeader(mList.getFixedFirstVisibleItem());
+ }
+ // invalidating the list will trigger dispatchDraw()
+ mList.invalidate();
+ }
+
+ public boolean areHeadersSticky() {
+ return mAreHeadersSticky;
+ }
+
+ /**
+ * Use areHeadersSticky() method instead
+ */
+ @Deprecated
+ public boolean getAreHeadersSticky() {
+ return areHeadersSticky();
+ }
+
+ public void setDrawingListUnderStickyHeader(
+ boolean drawingListUnderStickyHeader) {
+ mIsDrawingListUnderStickyHeader = drawingListUnderStickyHeader;
+ // reset the top clipping length
+ mList.setTopClippingLength(0);
+ }
+
+ public boolean isDrawingListUnderStickyHeader() {
+ return mIsDrawingListUnderStickyHeader;
+ }
+
+ public void setOnHeaderClickListener(
+ OnHeaderClickListener onHeaderClickListener) {
+ mOnHeaderClickListener = onHeaderClickListener;
+ if (mAdapter != null) {
+ if (mOnHeaderClickListener != null) {
+ mAdapter.setOnHeaderClickListener(new AdapterWrapperHeaderClickHandler());
+ } else {
+ mAdapter.setOnHeaderClickListener(null);
+ }
+ }
+ }
+
+ public View getListChildAt(int index) {
+ return mList.getChildAt(index);
+ }
+
+ public int getListChildCount() {
+ return mList.getChildCount();
+ }
+
+ /**
+ * Use the method with extreme caution!! Changing any values on the
+ * underlying ListView might break everything.
+ *
+ * @return the ListView backing this view.
+ */
+ public ListView getWrappedList() {
+ return mList;
+ }
+
+ /* ---------- ListView delegate methods ---------- */
+
+ public void setAdapter(StickyListHeadersAdapter adapter) {
+ if (adapter == null) {
+ mList.setAdapter(null);
+ clearHeader();
+ return;
+ }
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mDataSetObserver);
+ }
+
+ if (adapter instanceof SectionIndexer) {
+ mAdapter = new SectionIndexerAdapterWrapper(getContext(), adapter);
+ } else {
+ mAdapter = new AdapterWrapper(getContext(), adapter);
+ }
+ mDataSetObserver = new AdapterWrapperDataSetObserver();
+ mAdapter.registerDataSetObserver(mDataSetObserver);
+
+ if (mOnHeaderClickListener != null) {
+ mAdapter.setOnHeaderClickListener(new AdapterWrapperHeaderClickHandler());
+ } else {
+ mAdapter.setOnHeaderClickListener(null);
+ }
+
+ mAdapter.setDivider(mDivider, mDividerHeight);
+
+ mList.setAdapter(mAdapter);
+ clearHeader();
+ }
+
+ public StickyListHeadersAdapter getAdapter() {
+ return mAdapter == null ? null : mAdapter.mDelegate;
+ }
+
+ public void setDivider(Drawable divider) {
+ mDivider = divider;
+ if (mAdapter != null) {
+ mAdapter.setDivider(mDivider, mDividerHeight);
+ }
+ }
+
+ public void setDividerHeight(int dividerHeight) {
+ mDividerHeight = dividerHeight;
+ if (mAdapter != null) {
+ mAdapter.setDivider(mDivider, mDividerHeight);
+ }
+ }
+
+ public Drawable getDivider() {
+ return mDivider;
+ }
+
+ public int getDividerHeight() {
+ return mDividerHeight;
+ }
+
+ public void setOnScrollListener(OnScrollListener onScrollListener) {
+ mOnScrollListenerDelegate = onScrollListener;
+ }
+
+ public void setOnItemClickListener(OnItemClickListener listener) {
+ mList.setOnItemClickListener(listener);
+ }
+
+ public void setOnItemLongClickListener(OnItemLongClickListener listener) {
+ mList.setOnItemLongClickListener(listener);
+ }
+
+ public void addHeaderView(View v, Object data, boolean isSelectable) {
+ mList.addHeaderView(v, data, isSelectable);
+ }
+
+ public void addHeaderView(View v) {
+ mList.addHeaderView(v);
+ }
+
+ public void removeHeaderView(View v) {
+ mList.removeHeaderView(v);
+ }
+
+ public int getHeaderViewsCount() {
+ return mList.getHeaderViewsCount();
+ }
+
+ public void addFooterView(View v) {
+ mList.addFooterView(v);
+ }
+
+ public void removeFooterView(View v) {
+ mList.removeFooterView(v);
+ }
+
+ public int getFooterViewsCount() {
+ return mList.getFooterViewsCount();
+ }
+
+ public void setEmptyView(View v) {
+ mList.setEmptyView(v);
+ }
+
+ public View getEmptyView() {
+ return mList.getEmptyView();
+ }
+
+ @Override
+ public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled) {
+ mList.setVerticalScrollBarEnabled(verticalScrollBarEnabled);
+ }
+
+ @Override
+ public void setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled) {
+ mList.setHorizontalScrollBarEnabled(horizontalScrollBarEnabled);
+ }
+
+ @TargetApi(Build.VERSION_CODES.FROYO)
+ public void smoothScrollBy(int distance, int duration) {
+ requireSdkVersion(Build.VERSION_CODES.FROYO);
+ mList.smoothScrollBy(distance, duration);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public void smoothScrollByOffset(int offset) {
+ requireSdkVersion(Build.VERSION_CODES.HONEYCOMB);
+ mList.smoothScrollByOffset(offset);
+ }
+
+ @SuppressLint("NewApi")
+ @TargetApi(Build.VERSION_CODES.FROYO)
+ public void smoothScrollToPosition(int position) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ mList.smoothScrollToPosition(position);
+ } else {
+ int offset = mAdapter == null ? 0 : getHeaderOverlap(position);
+ offset -= mClippingToPadding ? 0 : mPaddingTop;
+ mList.smoothScrollToPositionFromTop(position, offset);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.FROYO)
+ public void smoothScrollToPosition(int position, int boundPosition) {
+ requireSdkVersion(Build.VERSION_CODES.FROYO);
+ mList.smoothScrollToPosition(position, boundPosition);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public void smoothScrollToPositionFromTop(int position, int offset) {
+ requireSdkVersion(Build.VERSION_CODES.HONEYCOMB);
+ offset += mAdapter == null ? 0 : getHeaderOverlap(position);
+ offset -= mClippingToPadding ? 0 : mPaddingTop;
+ mList.smoothScrollToPositionFromTop(position, offset);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public void smoothScrollToPositionFromTop(int position, int offset,
+ int duration) {
+ requireSdkVersion(Build.VERSION_CODES.HONEYCOMB);
+ offset += mAdapter == null ? 0 : getHeaderOverlap(position);
+ offset -= mClippingToPadding ? 0 : mPaddingTop;
+ mList.smoothScrollToPositionFromTop(position, offset, duration);
+ }
+
+ public void setSelection(int position) {
+ setSelectionFromTop(position, 0);
+ }
+
+ public void setSelectionAfterHeaderView() {
+ mList.setSelectionAfterHeaderView();
+ }
+
+ public void setSelectionFromTop(int position, int y) {
+ y += mAdapter == null ? 0 : getHeaderOverlap(position);
+ y -= mClippingToPadding ? 0 : mPaddingTop;
+ mList.setSelectionFromTop(position, y);
+ }
+
+ public void setSelector(Drawable sel) {
+ mList.setSelector(sel);
+ }
+
+ public void setSelector(int resID) {
+ mList.setSelector(resID);
+ }
+
+ public int getFirstVisiblePosition() {
+ return mList.getFirstVisiblePosition();
+ }
+
+ public int getLastVisiblePosition() {
+ return mList.getLastVisiblePosition();
+ }
+
+ public void setChoiceMode(int choiceMode) {
+ mList.setChoiceMode(choiceMode);
+ }
+
+ public void setItemChecked(int position, boolean value) {
+ mList.setItemChecked(position, value);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public int getCheckedItemCount() {
+ requireSdkVersion(Build.VERSION_CODES.HONEYCOMB);
+ return mList.getCheckedItemCount();
+ }
+
+ @TargetApi(Build.VERSION_CODES.FROYO)
+ public long[] getCheckedItemIds() {
+ requireSdkVersion(Build.VERSION_CODES.FROYO);
+ return mList.getCheckedItemIds();
+ }
+
+ public int getCheckedItemPosition() {
+ return mList.getCheckedItemPosition();
+ }
+
+ public SparseBooleanArray getCheckedItemPositions() {
+ return mList.getCheckedItemPositions();
+ }
+
+ public int getCount() {
+ return mList.getCount();
+ }
+
+ public Object getItemAtPosition(int position) {
+ return mList.getItemAtPosition(position);
+ }
+
+ public long getItemIdAtPosition(int position) {
+ return mList.getItemIdAtPosition(position);
+ }
+
+ public void setOnCreateContextMenuListener(OnCreateContextMenuListener l) {
+ mList.setOnCreateContextMenuListener(l);
+ }
+
+ public boolean showContextMenu() {
+ return mList.showContextMenu();
+ }
+
+ public void invalidateViews() {
+ mList.invalidateViews();
+ }
+
+ @Override
+ public void setClipToPadding(boolean clipToPadding) {
+ if (mList != null) {
+ mList.setClipToPadding(clipToPadding);
+ }
+ mClippingToPadding = clipToPadding;
+ }
+
+ @Override
+ public void setPadding(int left, int top, int right, int bottom) {
+ mPaddingLeft = left;
+ mPaddingTop = top;
+ mPaddingRight = right;
+ mPaddingBottom = bottom;
+
+ if (mList != null) {
+ mList.setPadding(left, top, right, bottom);
+ }
+ super.setPadding(0, 0, 0, 0);
+ requestLayout();
+ }
+
+ @Override
+ public int getPaddingLeft() {
+ return mPaddingLeft;
+ }
+
+ @Override
+ public int getPaddingTop() {
+ return mPaddingTop;
+ }
+
+ @Override
+ public int getPaddingRight() {
+ return mPaddingRight;
+ }
+
+ @Override
+ public int getPaddingBottom() {
+ return mPaddingBottom;
+ }
+
+ public void setFastScrollEnabled(boolean fastScrollEnabled) {
+ mList.setFastScrollEnabled(fastScrollEnabled);
+ }
+
+ /**
+ * @see android.widget.AbsListView#setFastScrollAlwaysVisible(boolean)
+ * @throws ApiLevelTooLowException on pre-Honeycomb device.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public void setFastScrollAlwaysVisible(boolean alwaysVisible) {
+ requireSdkVersion(Build.VERSION_CODES.HONEYCOMB);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ mList.setFastScrollAlwaysVisible(alwaysVisible);
+ }
+ }
+
+ /**
+ * @see android.widget.AbsListView#isFastScrollAlwaysVisible()
+ * @return true if the fast scroller will always show. False on pre-Honeycomb devices.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public boolean isFastScrollAlwaysVisible(){
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ return false;
+ }
+ return mList.isFastScrollAlwaysVisible();
+ }
+
+ private void requireSdkVersion(int versionCode) {
+ if (Build.VERSION.SDK_INT < versionCode) {
+ throw new ApiLevelTooLowException(versionCode);
+ }
+ }
+
+ public int getPositionForView(View view) {
+ return mList.getPositionForView(view);
+ }
+
+
+
+
+
+ private int mTotalCount;
+ private int mItemOffsetY[];
+ private boolean scrollIsComputed = false;
+ private int mHeight;
+
+
+ public int getListHeight() {
+ return mHeight;
+ }
+
+ public void computeScrollY() {
+ mHeight = 0;
+ mTotalCount = getAdapter().getCount();
+
+ int sectionHeight = 0;
+ int itemHeight = 0;
+ int desiredWidth = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST);
+
+ if (mItemOffsetY == null) {
+ mItemOffsetY = new int[mTotalCount];
+ }
+ for (int i = 0; i < mTotalCount; ++i) {
+
+ if (i == 0) {
+ View view = getAdapter().getView(i, null, this);
+ view.measure(desiredWidth, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ sectionHeight = view.getMeasuredHeight();
+ mItemOffsetY[i] = mHeight;
+ mHeight += sectionHeight;
+ } else if (i == 1) {
+ View view = getAdapter().getView(i, null, this);
+ view.measure(desiredWidth, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ itemHeight = view.getMeasuredHeight();
+ mItemOffsetY[i] = mHeight;
+ mHeight += itemHeight;
+ } else {
+ int type = getAdapter().getItemViewType(i);
+ switch (type) {
+ case ContactsAdapter.TYPE_CONTACT:
+ mHeight += itemHeight;
+ case ContactsAdapter.TYPE_HEADER:
+ mHeight += sectionHeight;
+ }
+ mItemOffsetY[i] = mHeight;
+ mHeight += sectionHeight;
+ }
+
+ System.out.println(mHeight);
+ }
+ scrollIsComputed = true;
+ }
+
+ public boolean scrollYIsComputed() {
+ return scrollIsComputed;
+ }
+
+ public int getComputedScrollY() {
+ int pos, nScrollY, nItemY;
+ View view = null;
+ pos = getFirstVisiblePosition();
+ view = getChildAt(0);
+ nItemY = view.getTop();
+ nScrollY = mItemOffsetY[pos] - nItemY;
+ return nScrollY;
+ }
+
+}
diff --git a/ring-android/src/cx/ring/views/stickylistheaders/WrapperView.java b/ring-android/src/cx/ring/views/stickylistheaders/WrapperView.java
new file mode 100644
index 0000000..9ee36ad
--- /dev/null
+++ b/ring-android/src/cx/ring/views/stickylistheaders/WrapperView.java
@@ -0,0 +1,150 @@
+package cx.ring.views.stickylistheaders;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+/**
+ *
+ * the view that wrapps a divider header and a normal list item. The listview sees this as 1 item
+ *
+ * @author Emil Sjölander
+ */
+public class WrapperView extends ViewGroup {
+
+ View mItem;
+ Drawable mDivider;
+ int mDividerHeight;
+ View mHeader;
+ int mItemTop;
+
+ WrapperView(Context c) {
+ super(c);
+ }
+
+ public boolean hasHeader() {
+ return mHeader != null;
+ }
+
+ public View getItem() {
+ return mItem;
+ }
+
+ public View getHeader() {
+ return mHeader;
+ }
+
+ void update(View item, View header, Drawable divider, int dividerHeight) {
+
+ //every wrapperview must have a list item
+ if (item == null) {
+ throw new NullPointerException("List view item must not be null.");
+ }
+
+ //only remove the current item if it is not the same as the new item. this can happen if wrapping a recycled view
+ if (this.mItem != item) {
+ removeView(this.mItem);
+ this.mItem = item;
+ final ViewParent parent = item.getParent();
+ if(parent != null && parent != this) {
+ if(parent instanceof ViewGroup) {
+ ((ViewGroup) parent).removeView(item);
+ }
+ }
+ addView(item);
+ }
+
+ //same logik as above but for the header
+ if (this.mHeader != header) {
+ if (this.mHeader != null) {
+ removeView(this.mHeader);
+ }
+ this.mHeader = header;
+ if (header != null) {
+ addView(header);
+ }
+ }
+
+ if (this.mDivider != divider) {
+ this.mDivider = divider;
+ this.mDividerHeight = dividerHeight;
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth,
+ MeasureSpec.EXACTLY);
+ int measuredHeight = 0;
+
+ //measure header or divider. when there is a header visible it acts as the divider
+ if (mHeader != null) {
+ ViewGroup.LayoutParams params = mHeader.getLayoutParams();
+ if (params != null && params.height > 0) {
+ mHeader.measure(childWidthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY));
+ } else {
+ mHeader.measure(childWidthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ }
+ measuredHeight += mHeader.getMeasuredHeight();
+ } else if (mDivider != null) {
+ measuredHeight += mDividerHeight;
+ }
+
+ //measure item
+ ViewGroup.LayoutParams params = mItem.getLayoutParams();
+ if (params != null && params.height > 0) {
+ mItem.measure(childWidthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY));
+ } else {
+ mItem.measure(childWidthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ }
+ measuredHeight += mItem.getMeasuredHeight();
+
+ setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+
+ l = 0;
+ t = 0;
+ r = getWidth();
+ b = getHeight();
+
+ if (mHeader != null) {
+ int headerHeight = mHeader.getMeasuredHeight();
+ mHeader.layout(l, t, r, headerHeight);
+ mItemTop = headerHeight;
+ mItem.layout(l, headerHeight, r, b);
+ } else if (mDivider != null) {
+ mDivider.setBounds(l, t, r, mDividerHeight);
+ mItemTop = mDividerHeight;
+ mItem.layout(l, mDividerHeight, r, b);
+ } else {
+ mItemTop = t;
+ mItem.layout(l, t, r, b);
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+ if (mHeader == null && mDivider != null) {
+ // Drawable.setBounds() does not seem to work pre-honeycomb. So have
+ // to do this instead
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ canvas.clipRect(0, 0, getWidth(), mDividerHeight);
+ }
+ mDivider.draw(canvas);
+ }
+ }
+}
diff --git a/ring-android/src/cx/ring/views/stickylistheaders/WrapperViewList.java b/ring-android/src/cx/ring/views/stickylistheaders/WrapperViewList.java
new file mode 100644
index 0000000..c0c95d7
--- /dev/null
+++ b/ring-android/src/cx/ring/views/stickylistheaders/WrapperViewList.java
@@ -0,0 +1,176 @@
+package cx.ring.views.stickylistheaders;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.AbsListView;
+import android.widget.ListView;
+
+class WrapperViewList extends ListView {
+
+ interface LifeCycleListener {
+ void onDispatchDrawOccurred(Canvas canvas);
+ }
+
+ private LifeCycleListener mLifeCycleListener;
+ private List<View> mFooterViews;
+ private int mTopClippingLength;
+ private Rect mSelectorRect = new Rect();// for if reflection fails
+ private Field mSelectorPositionField;
+ private boolean mClippingToPadding = true;
+
+ public WrapperViewList(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // Use reflection to be able to change the size/position of the list
+ // selector so it does not come under/over the header
+ try {
+ Field selectorRectField = AbsListView.class.getDeclaredField("mSelectorRect");
+ selectorRectField.setAccessible(true);
+ mSelectorRect = (Rect) selectorRectField.get(this);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ mSelectorPositionField = AbsListView.class.getDeclaredField("mSelectorPosition");
+ mSelectorPositionField.setAccessible(true);
+ }
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public boolean performItemClick(View view, int position, long id) {
+ if (view instanceof WrapperView) {
+ view = ((WrapperView) view).mItem;
+ }
+ return super.performItemClick(view, position, id);
+ }
+
+ private void positionSelectorRect() {
+ if (!mSelectorRect.isEmpty()) {
+ int selectorPosition = getSelectorPosition();
+ if (selectorPosition >= 0) {
+ int firstVisibleItem = getFixedFirstVisibleItem();
+ View v = getChildAt(selectorPosition - firstVisibleItem);
+ if (v instanceof WrapperView) {
+ WrapperView wrapper = ((WrapperView) v);
+ mSelectorRect.top = wrapper.getTop() + wrapper.mItemTop;
+ }
+ }
+ }
+ }
+
+ private int getSelectorPosition() {
+ if (mSelectorPositionField == null) { // not all supported andorid
+ // version have this variable
+ for (int i = 0; i < getChildCount(); i++) {
+ if (getChildAt(i).getBottom() == mSelectorRect.bottom) {
+ return i + getFixedFirstVisibleItem();
+ }
+ }
+ } else {
+ try {
+ return mSelectorPositionField.getInt(this);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ positionSelectorRect();
+ if (mTopClippingLength != 0) {
+ canvas.save();
+ Rect clipping = canvas.getClipBounds();
+ clipping.top = mTopClippingLength;
+ canvas.clipRect(clipping);
+ super.dispatchDraw(canvas);
+ canvas.restore();
+ } else {
+ super.dispatchDraw(canvas);
+ }
+ mLifeCycleListener.onDispatchDrawOccurred(canvas);
+ }
+
+ void setLifeCycleListener(LifeCycleListener lifeCycleListener) {
+ mLifeCycleListener = lifeCycleListener;
+ }
+
+ @Override
+ public void addFooterView(View v) {
+ super.addFooterView(v);
+ if (mFooterViews == null) {
+ mFooterViews = new ArrayList<View>();
+ }
+ mFooterViews.add(v);
+ }
+
+ @Override
+ public boolean removeFooterView(View v) {
+ if (super.removeFooterView(v)) {
+ mFooterViews.remove(v);
+ return true;
+ }
+ return false;
+ }
+
+ boolean containsFooterView(View v) {
+ if (mFooterViews == null) {
+ return false;
+ }
+ return mFooterViews.contains(v);
+ }
+
+ void setTopClippingLength(int topClipping) {
+ mTopClippingLength = topClipping;
+ }
+
+ int getFixedFirstVisibleItem() {
+ int firstVisibleItem = getFirstVisiblePosition();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ return firstVisibleItem;
+ }
+
+ // first getFirstVisiblePosition() reports items
+ // outside the view sometimes on old versions of android
+ for (int i = 0; i < getChildCount(); i++) {
+ if (getChildAt(i).getBottom() >= 0) {
+ firstVisibleItem += i;
+ break;
+ }
+ }
+
+ // work around to fix bug with firstVisibleItem being to high
+ // because list view does not take clipToPadding=false into account
+ // on old versions of android
+ if (!mClippingToPadding && getPaddingTop() > 0 && firstVisibleItem > 0) {
+ if (getChildAt(0).getTop() > 0) {
+ firstVisibleItem -= 1;
+ }
+ }
+
+ return firstVisibleItem;
+ }
+
+ @Override
+ public void setClipToPadding(boolean clipToPadding) {
+ mClippingToPadding = clipToPadding;
+ super.setClipToPadding(clipToPadding);
+ }
+
+}