diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 3040df5..259493a 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -19,9 +19,8 @@ - @@ -29,7 +28,14 @@ + + + diff --git a/android/res/drawable-hdpi/history_browser_item_indicator.png b/android/res/drawable-hdpi/history_browser_item_indicator.png new file mode 100755 index 0000000..0d43042 Binary files /dev/null and b/android/res/drawable-hdpi/history_browser_item_indicator.png differ diff --git a/android/res/drawable-hdpi/history_maps_item_indicator.png b/android/res/drawable-hdpi/history_maps_item_indicator.png new file mode 100755 index 0000000..7aaa848 Binary files /dev/null and b/android/res/drawable-hdpi/history_maps_item_indicator.png differ diff --git a/android/res/drawable-hdpi/history_yt_item_indicator.png b/android/res/drawable-hdpi/history_yt_item_indicator.png new file mode 100755 index 0000000..0c7a88a Binary files /dev/null and b/android/res/drawable-hdpi/history_yt_item_indicator.png differ diff --git a/android/res/drawable-mdpi/history_browser_item_indicator.png b/android/res/drawable-mdpi/history_browser_item_indicator.png new file mode 100755 index 0000000..7041099 Binary files /dev/null and b/android/res/drawable-mdpi/history_browser_item_indicator.png differ diff --git a/android/res/drawable-mdpi/history_maps_item_indicator.png b/android/res/drawable-mdpi/history_maps_item_indicator.png new file mode 100755 index 0000000..987b641 Binary files /dev/null and b/android/res/drawable-mdpi/history_maps_item_indicator.png differ diff --git a/android/res/drawable-mdpi/history_yt_item_indicator.png b/android/res/drawable-mdpi/history_yt_item_indicator.png new file mode 100755 index 0000000..d4f2d3f Binary files /dev/null and b/android/res/drawable-mdpi/history_yt_item_indicator.png differ diff --git a/android/res/layout/history.xml b/android/res/layout/history.xml new file mode 100644 index 0000000..f32fbcb --- /dev/null +++ b/android/res/layout/history.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/android/res/layout/history_header.xml b/android/res/layout/history_header.xml new file mode 100644 index 0000000..9a177b0 --- /dev/null +++ b/android/res/layout/history_header.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/android/res/layout/history_item.xml b/android/res/layout/history_item.xml new file mode 100644 index 0000000..a4f5394 --- /dev/null +++ b/android/res/layout/history_item.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/android/res/menu/history.xml b/android/res/menu/history.xml new file mode 100644 index 0000000..b2a1de6 --- /dev/null +++ b/android/res/menu/history.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/android/res/menu/help.xml b/android/res/menu/setup.xml similarity index 96% rename from android/res/menu/help.xml rename to android/res/menu/setup.xml index 342f0f3..a6935fc 100644 --- a/android/res/menu/help.xml +++ b/android/res/menu/setup.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/android/res/values/arrays.xml b/android/res/values/arrays.xml new file mode 100644 index 0000000..e05592d --- /dev/null +++ b/android/res/values/arrays.xml @@ -0,0 +1,11 @@ + + + + + Open + Add bookmark + Share link + Copy link url + Remove from history + + \ No newline at end of file diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml index 6d3b33a..4d77231 100644 --- a/android/res/values/strings.xml +++ b/android/res/values/strings.xml @@ -77,18 +77,6 @@ Copied desktop clipboard - - Sending link... - - - Link sent - - - Link not sent - - - Link not sent - authentication required - Chrome to Phone lets you easily share links, maps, and currently selected phone numbers and text @@ -117,4 +105,38 @@ <a href="{tos_link}">Terms of Service</a> and <a href="{pp_link}">Privacy Policy</a>. + + + Today + + + Last 7 days + + + Last month + + + Older + + + Clear all + + + Settings + + + Share with + + + Sending link... + + + Link sent + + + Link not sent + + + Link not sent - authentication required + diff --git a/android/src/com/google/android/apps/chrometophone/AppEngineClient.java b/android/src/com/google/android/apps/chrometophone/AppEngineClient.java index b7c0005..4bcf071 100644 --- a/android/src/com/google/android/apps/chrometophone/AppEngineClient.java +++ b/android/src/com/google/android/apps/chrometophone/AppEngineClient.java @@ -132,7 +132,7 @@ public class AppEngineClient { // User will be asked for "App Engine" permission. if (authToken == null) { // No auth token - will need to ask permission from user. - Intent intent = new Intent(MainActivity.AUTH_PERMISSION_ACTION); + Intent intent = new Intent(SetupActivity.AUTH_PERMISSION_ACTION); intent.putExtra("AccountManagerBundle", bundle); context.sendBroadcast(intent); } diff --git a/android/src/com/google/android/apps/chrometophone/C2DMReceiver.java b/android/src/com/google/android/apps/chrometophone/C2DMReceiver.java index 0761ee6..85dcc51 100644 --- a/android/src/com/google/android/apps/chrometophone/C2DMReceiver.java +++ b/android/src/com/google/android/apps/chrometophone/C2DMReceiver.java @@ -21,19 +21,10 @@ import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.media.AudioManager; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; import android.os.Bundle; -import android.text.ClipboardManager; import com.google.android.c2dm.C2DMBaseReceiver; @@ -61,147 +52,55 @@ public class C2DMReceiver extends C2DMBaseReceiver { @Override public void onMessage(Context context, Intent intent) { - Bundle extras = intent.getExtras(); - if (extras != null) { - String url = (String) extras.get("url"); - String title = (String) extras.get("title"); - String sel = (String) extras.get("sel"); - String debug = (String) extras.get("debug"); + Bundle extras = intent.getExtras(); + if (extras != null) { + String url = (String) extras.get("url"); + String title = (String) extras.get("title"); + String sel = (String) extras.get("sel"); + String debug = (String) extras.get("debug"); - if (debug != null) { - // server-controlled debug - the server wants to know - // we received the message, and when. This is not user-controllable, - // we don't want extra traffic on the server or phone. Server may - // turn this on for a small percentage of requests or for users - // who report issues. - DefaultHttpClient client = new DefaultHttpClient(); - HttpGet get = new HttpGet(AppEngineClient.BASE_URL + "/debug?id=" - + extras.get("collapse_key")); - // No auth - the purpose is only to generate a log/confirm delivery - // (to avoid overhead of getting the token) - try { - client.execute(get); - } catch (ClientProtocolException e) { - // ignore - } catch (IOException e) { - // ignore - } - } - - if (title != null && url != null && url.startsWith("http")) { - SharedPreferences settings = Prefs.get(context); - Intent launchIntent = getLaunchIntent(context, url, title, sel); - - if (settings.getBoolean("launchBrowserOrMaps", true) && launchIntent != null) { - playNotificationSound(context); - context.startActivity(launchIntent); - } else { - if (sel != null && sel.length() > 0) { // have selection - generateNotification(context, sel, - context.getString(R.string.copied_desktop_clipboard), launchIntent); - } else { - generateNotification(context, url, title, launchIntent); - } - } - } - } - } - - private Intent getLaunchIntent(Context context, String url, String title, String sel) { - Intent intent = null; - String number = parseTelephoneNumber(sel); - if (number != null) { - intent = new Intent("android.intent.action.DIAL", - Uri.parse("tel:" + number)); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - ClipboardManager cm = - (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); - cm.setText(number); - } else if (sel != null && sel.length() > 0) { - // No intent for selection - just copy to clipboard - ClipboardManager cm = - (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); - cm.setText(sel); - } else { - final String GMM_PACKAGE_NAME = "com.google.android.apps.maps"; - final String GMM_CLASS_NAME = "com.google.android.maps.MapsActivity"; - - intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (isMapsURL(url)) { - intent.setClassName(GMM_PACKAGE_NAME, GMM_CLASS_NAME); + if (debug != null) { + // server-controlled debug - the server wants to know + // we received the message, and when. This is not user-controllable, + // we don't want extra traffic on the server or phone. Server may + // turn this on for a small percentage of requests or for users + // who report issues. + DefaultHttpClient client = new DefaultHttpClient(); + HttpGet get = new HttpGet(AppEngineClient.BASE_URL + "/debug?id=" + + extras.get("collapse_key")); + // No auth - the purpose is only to generate a log/confirm delivery + // (to avoid overhead of getting the token) + try { + client.execute(get); + } catch (ClientProtocolException e) { + // ignore + } catch (IOException e) { + // ignore + } } - // Fall back if we can't resolve intent (i.e. app missing) - PackageManager pm = context.getPackageManager(); - if (null == intent.resolveActivity(pm)) { - intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (title != null && url != null && url.startsWith("http")) { + SharedPreferences settings = Prefs.get(context); + Intent launchIntent = LauncherUtils.getLaunchIntent(context, title, url, sel); + + // Notify and optionally start activity + if (settings.getBoolean("launchBrowserOrMaps", true) && launchIntent != null) { + LauncherUtils.playNotificationSound(context); + context.startActivity(launchIntent); + } else { + if (sel != null && sel.length() > 0) { // have selection + LauncherUtils.generateNotification(context, sel, + context.getString(R.string.copied_desktop_clipboard), launchIntent); + } else { + LauncherUtils.generateNotification(context, url, title, launchIntent); + } + } + + // Record history (for link/maps only) + if (launchIntent != null && launchIntent.getAction().equals(Intent.ACTION_VIEW)) { + HistoryDatabase.get(context).insertHistory(title, url); + } } } - return intent; } - - private void generateNotification(Context context, String msg, String title, Intent intent) { - int icon = R.drawable.status_icon; - long when = System.currentTimeMillis(); - - Notification notification = new Notification(icon, title, when); - notification.setLatestEventInfo(context, title, msg, - PendingIntent.getActivity(context, 0, intent, 0)); - notification.flags |= Notification.FLAG_AUTO_CANCEL; - - SharedPreferences settings = Prefs.get(context); - int notificatonID = settings.getInt("notificationID", 0); // allow multiple notifications - - NotificationManager nm = - (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(notificatonID, notification); - playNotificationSound(context); - - SharedPreferences.Editor editor = settings.edit(); - editor.putInt("notificationID", ++notificatonID % 32); - editor.commit(); - } - - private void playNotificationSound(Context context) { - Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); - if (uri != null) { - Ringtone rt = RingtoneManager.getRingtone(context, uri); - if (rt != null) { - rt.setStreamType(AudioManager.STREAM_NOTIFICATION); - rt.play(); - } - } - } - - private String parseTelephoneNumber(String sel) { - if (sel == null || sel.length() == 0) return null; - - // Hack: Remove trailing left-to-right mark (Google Maps adds this) - if (sel.codePointAt(sel.length() - 1) == 8206) { - sel = sel.substring(0, sel.length() - 1); - } - - String number = null; - if (sel.matches("([Tt]el[:]?)?\\s?[+]?(\\(?[0-9|\\s|\\-|\\.]\\)?)+")) { - String elements[] = sel.split("([Tt]el[:]?)"); - number = elements.length > 1 ? elements[1] : elements[0]; - number = number.replace(" ", ""); - - // Remove option (0) in international numbers, e.g. +44 (0)20 ... - if (number.matches("\\+[0-9]{2,3}\\(0\\).*")) { - int openBracket = number.indexOf('('); - int closeBracket = number.indexOf(')'); - number = number.substring(0, openBracket) + - number.substring(closeBracket + 1); - } - } - return number; - } - - private boolean isMapsURL(String url) { - return url.matches("http://maps\\.google\\.[a-z]{2,3}(\\.[a-z]{2})?[/?].*") || - url.matches("http://www\\.google\\.[a-z]{2,3}(\\.[a-z]{2})?/maps.*"); - } } diff --git a/android/src/com/google/android/apps/chrometophone/HistoryActivity.java b/android/src/com/google/android/apps/chrometophone/HistoryActivity.java new file mode 100644 index 0000000..b40dd6a --- /dev/null +++ b/android/src/com/google/android/apps/chrometophone/HistoryActivity.java @@ -0,0 +1,341 @@ +package com.google.android.apps.chrometophone; + +import java.util.Calendar; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.Bundle; +import android.provider.Browser; +import android.text.ClipboardManager; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseExpandableListAdapter; +import android.widget.ExpandableListView; +import android.widget.TextView; +import android.widget.ExpandableListView.OnChildClickListener; + +/** + * Activity that shows the history of links received. + */ +public class HistoryActivity extends Activity implements OnChildClickListener { + private static final int DIALOG_LINK_ACTION = 1; + + private ExpandableListView mList; + private HistoryExpandableListAdapter mListAdapter; + private Context mContext = null; + + private Link mSelectedLink; + private int mSelectedGroup; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Run the setup first if necessary + SharedPreferences prefs = Prefs.get(this); + if (prefs.getString("deviceRegistrationID", null) == null) { + startActivity(new Intent(this, SetupActivity.class)); + } + + setContentView(R.layout.history); + mList = (ExpandableListView) findViewById(android.R.id.list); + mList.setOnCreateContextMenuListener(this); + mList.setOnChildClickListener(this); + + mListAdapter = new HistoryExpandableListAdapter(this); + mList.setAdapter(mListAdapter); + + mContext = this; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.history, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.clear: { + HistoryDatabase.get(this).deleteAllHistory(); + mListAdapter.refresh(); + for (int i = 0; i < DateBinSorter.NUM_BINS; i++) { + mList.collapseGroup(i); + } + return true; + } + case R.id.settings: { + startActivity(new Intent(this, SetupActivity.class)); + return true; + } + case R.id.help: { + startActivity(new Intent(this, HelpActivity.class)); + return true; + } + default: { + return super.onOptionsItemSelected(item); + } + } + } + + public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, + int childPosition, long id) { + mSelectedLink = mListAdapter.getLinkAtPosition(groupPosition, childPosition); + mSelectedGroup = groupPosition; + showDialog(DIALOG_LINK_ACTION); + return true; + } + + @Override + protected void onResume() { + super.onResume(); + mListAdapter.refresh(); + } + + @Override + protected Dialog onCreateDialog(int id) { + switch (id) { + case DIALOG_LINK_ACTION: + return new AlertDialog.Builder(this) + .setTitle(ellipsis(mSelectedLink.mTitle)) + .setItems(R.array.link_action_dialog_items, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { // Open + startActivity(LauncherUtils.getLaunchIntent(mContext, + mSelectedLink.mTitle, mSelectedLink.mUrl, null)); + } else if (which == 1) { // Add bookmark + Browser.saveBookmark(mContext, mSelectedLink.mTitle, + mSelectedLink.mUrl); + } else if (which == 2) { // Share link + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, mSelectedLink.mUrl); + intent.setType("text/plain"); + startActivity(Intent.createChooser(intent, + getString(R.string.share_chooser_title))); + } else if (which == 3) { // Copy link URL + ClipboardManager cm = + (ClipboardManager) mContext.getSystemService(CLIPBOARD_SERVICE); + cm.setText(mSelectedLink.mUrl); + } else if (which == 4) { // Remove from history + HistoryDatabase.get(mContext).deleteHistory(mSelectedLink.mUrl); + mListAdapter.refresh(); + mList.collapseGroup(mSelectedGroup); + mList.expandGroup(mSelectedGroup); + } + } + }).create(); + } + return null; + } + + @Override + protected void onPrepareDialog(int id, Dialog dialog) { + dialog.setTitle(ellipsis(mSelectedLink.mTitle)); + } + + private String ellipsis(String string) { + int MAX_LENGTH = 50; + if (string.length() > MAX_LENGTH - 3) { + string = string.substring(0, MAX_LENGTH - 3); + string += "..."; + } + return string; + } + + class HistoryExpandableListAdapter extends BaseExpandableListAdapter { + private final Context mContext; + private Cursor mCursor; + private DateBinSorter mDateBinSorter; + private int mChildCounts[]; + + public HistoryExpandableListAdapter(Context context) { + this.mContext = context; + refresh(); + } + + public void refresh() { + mCursor = HistoryDatabase.get(mContext).lookupHistory(); + mDateBinSorter = new DateBinSorter(mContext); + calculateCounts(); + } + + private void calculateCounts() { + mChildCounts = new int[DateBinSorter.NUM_BINS]; + for (int j = 0; j < DateBinSorter.NUM_BINS; j++) { + mChildCounts[j] = 0; + } + + int dateIndex = -1; + if (mCursor.moveToFirst() && mCursor.getCount() > 0) { + while (!mCursor.isAfterLast()) { + long date = mCursor.getLong(HistoryDatabase.RECEIVE_TIME_INDEX); + int index = mDateBinSorter.getBin(date); + if (index > dateIndex) { + if (index == DateBinSorter.NUM_BINS - 1) { + mChildCounts[index] = mCursor.getCount() - mCursor.getPosition(); + break; + } + dateIndex = index; + } + mChildCounts[dateIndex]++; + mCursor.moveToNext(); + } + } + } + + public Object getChild(int groupPosition, int childPosition) { + return null; + } + + public long getChildId(int groupPosition, int childPosition) { + return moveCursorPosition(groupPosition, childPosition); + } + + public View getChildView(int groupPosition, int childPosition, boolean isLastChild, + View convertView, ViewGroup parent) { + HistoryItemView itemView; + if (null == convertView || !(convertView instanceof HistoryItemView)) { + itemView = new HistoryItemView(mContext); + // Add padding on the left so it will be indented from the + // arrows on the group views. + itemView.setPadding(itemView.getPaddingLeft() + 10, + itemView.getPaddingTop(), + itemView.getPaddingRight(), + itemView.getPaddingBottom()); + } else { + itemView = (HistoryItemView) convertView; + } + + + moveCursorPosition(groupPosition, childPosition); + itemView.setTitle(mCursor.getString(HistoryDatabase.TITLE_INDEX)); + itemView.setUrl(mCursor.getString(HistoryDatabase.URL_INDEX)); + + return itemView; + } + + public int getChildrenCount(int groupPosition) { + return mChildCounts[groupPosition]; + } + + public Object getGroup(int groupPosition) { + return null; + } + + public int getGroupCount() { + return DateBinSorter.NUM_BINS; + } + + public long getGroupId(int groupPosition) { + return groupPosition; + } + + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, + ViewGroup parent) { + TextView item; + if (null == convertView || !(convertView instanceof TextView)) { + LayoutInflater factory = LayoutInflater.from(mContext); + item = (TextView) factory.inflate(R.layout.history_header, null); + } else { + item = (TextView) convertView; + } + String label = mDateBinSorter.getBinLabel(groupPosition); + item.setText(label); + return item; + } + + public boolean hasStableIds() { + return true; + } + + public boolean isChildSelectable(int groupPosition, int childPosition) { + return true; + } + + private int moveCursorPosition(int groupPosition, int childPosition) { + int index = childPosition; + for (int i = 0; i < groupPosition; i++) { + index += mChildCounts[i]; + } + mCursor.moveToPosition(index); + return index; + } + + private Link getLinkAtPosition(int groupPosition, int childPosition) { + moveCursorPosition(groupPosition, childPosition); + return new Link(mCursor.getString(HistoryDatabase.TITLE_INDEX), + mCursor.getString(HistoryDatabase.URL_INDEX)); + } + } + + class DateBinSorter { + public static final int NUM_BINS = 4; + private final long [] mBins = new long[NUM_BINS-1]; + private final Context mContext; + + public DateBinSorter(Context context) { + this.mContext = context; + + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + mBins[0] = c.getTimeInMillis(); // today + + c.add(Calendar.DAY_OF_YEAR, -7); + mBins[1] = c.getTimeInMillis(); // seven days ago + + c.add(Calendar.DAY_OF_YEAR, 7); + c.add(Calendar.MONTH, -1); + mBins[2] = c.getTimeInMillis(); // one month ago + } + + /** + * Get the date bin for a specified time: + * 0 => today, 1 => last week, + * 2 => last month, 3 => older + */ + public int getBin(long time) { + int lastDay = NUM_BINS - 1; + for (int i = 0; i < lastDay; i++) { + if (time > mBins[i]) return i; + } + return lastDay; + } + + private String getBinLabel(int index) { + if (index == 0) { + return mContext.getString(R.string.today_text); + } else if (index == 1) { + return mContext.getString(R.string.last_seven_days_text); + } else if (index == 2) { + return mContext.getString(R.string.last_month_text); + } else { + return mContext.getString(R.string.older_text); + } + } + } + + class Link { + public final String mTitle; + public final String mUrl; + + public Link(String title, String url) { + mTitle = title; + mUrl = url; + } + } +} diff --git a/android/src/com/google/android/apps/chrometophone/HistoryDatabase.java b/android/src/com/google/android/apps/chrometophone/HistoryDatabase.java new file mode 100644 index 0000000..d4bfb1c --- /dev/null +++ b/android/src/com/google/android/apps/chrometophone/HistoryDatabase.java @@ -0,0 +1,106 @@ +package com.google.android.apps.chrometophone; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteStatement; + +/** + * Database to store and retrieve history of received links. + */ +public class HistoryDatabase { + public static final int URL_INDEX = 0; + public static final int TITLE_INDEX = 1; + public static final int RECEIVE_TIME_INDEX = 2; + + private static final String DATABASE_NAME = "history.db"; + private static final int DATABASE_VERSION = 1; + + private static final String TABLE_NAME = "history"; + private static final String URL_COL_NAME = "url"; + private static final String TITLE_COL_NAME = "title"; + private static final String RECEIVE_TIME_COL_NAME = "receive_time"; + + private static final String[] ALL_COLUMNS = + new String[] { URL_COL_NAME, TITLE_COL_NAME, RECEIVE_TIME_COL_NAME }; + + private final SQLiteDatabase mDb; + private SQLiteStatement mInsertStatement; + private SQLiteStatement mDeleteStatement; + private SQLiteStatement mDeleteAllStatement; + + private static HistoryDatabase mSingleton; + + private HistoryDatabase(Context context) { + mDb = new DatabaseHelper(context).getWritableDatabase(); + } + + public synchronized static HistoryDatabase get(Context context) { + if (mSingleton == null) { + mSingleton = new HistoryDatabase(context); + } + return mSingleton; + } + + public void insertHistory(String title, String url) { + if (mInsertStatement == null) { + mInsertStatement = mDb.compileStatement("INSERT OR REPLACE INTO "+ TABLE_NAME + + "(" + URL_COL_NAME + ", " + + TITLE_COL_NAME + ", " + + RECEIVE_TIME_COL_NAME + + ") VALUES (?, ?, ?)"); + } + + mInsertStatement.bindString(1, url); + mInsertStatement.bindString(2, title); + mInsertStatement.bindLong(3, System.currentTimeMillis()); + mInsertStatement.execute(); + } + + public Cursor lookupHistory() { + return mDb.query(TABLE_NAME, ALL_COLUMNS, null, null, null, null, null); + } + + public void deleteHistory(String url) { + if (mDeleteStatement == null) { + mDeleteStatement = mDb.compileStatement("DELETE FROM "+ TABLE_NAME + + " WHERE " + URL_COL_NAME + " == ?"); + } + mDeleteStatement.bindString(1, url); + mDeleteStatement.execute(); + } + + public void deleteAllHistory() { + if (mDeleteAllStatement == null) { + mDeleteAllStatement = mDb.compileStatement("DELETE FROM "+ TABLE_NAME); + } + mDeleteAllStatement.execute(); + } + + /** + * Database helper that creates and maintains the SQLite database. + */ + static class DatabaseHelper extends SQLiteOpenHelper { + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + createHistoryTable(db); + } + + private void createHistoryTable(SQLiteDatabase db) { + db.execSQL("CREATE TABLE "+ TABLE_NAME + "(" + + URL_COL_NAME + " TEXT PRIMARY KEY, " + + TITLE_COL_NAME + " TEXT NOT NULL, " + + RECEIVE_TIME_COL_NAME + " INTEGER)"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // Implement the upgrade path to databases with DATABASE_VERSION > 1 here + } + } +} diff --git a/android/src/com/google/android/apps/chrometophone/HistoryItemView.java b/android/src/com/google/android/apps/chrometophone/HistoryItemView.java new file mode 100644 index 0000000..11b3de5 --- /dev/null +++ b/android/src/com/google/android/apps/chrometophone/HistoryItemView.java @@ -0,0 +1,54 @@ +package com.google.android.apps.chrometophone; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * A single history item (child view for expandable list). + */ +public class HistoryItemView extends LinearLayout { + private final TextView mTextView; + private final TextView mUrlText; + private final ImageView mImageView; + private final Context mContext; + + public HistoryItemView(Context context) { + super(context); + mContext = context; + LayoutInflater factory = LayoutInflater.from(context); + factory.inflate(R.layout.history_item, this); + mTextView = (TextView) findViewById(R.id.title); + mUrlText = (TextView) findViewById(R.id.url); + mImageView = (ImageView) findViewById(R.id.icon); + } + + public void setTitle(String title) { + mTextView.setText(ellipsis(title)); + } + + public void setUrl(String url) { + int id = R.drawable.history_browser_item_indicator; + if (LauncherUtils.isMapsURL(url)) { + id = R.drawable.history_maps_item_indicator; + } else if (LauncherUtils.isYouTubeURL(url)) { + id = R.drawable.history_yt_item_indicator; + } + Drawable icon = mContext.getResources().getDrawable(id); + mImageView.setImageDrawable(icon); + mUrlText.setText(ellipsis(url)); + } + + private String ellipsis(String string) { + int MAX_LENGTH = 50; + if (string.length() > MAX_LENGTH - 3) { + string = string.substring(0, MAX_LENGTH - 3); + string += "..."; + } + return string; + } +} + diff --git a/android/src/com/google/android/apps/chrometophone/LauncherUtils.java b/android/src/com/google/android/apps/chrometophone/LauncherUtils.java new file mode 100644 index 0000000..e29fc3a --- /dev/null +++ b/android/src/com/google/android/apps/chrometophone/LauncherUtils.java @@ -0,0 +1,121 @@ +package com.google.android.apps.chrometophone; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.media.AudioManager; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.text.ClipboardManager; + +/** + * Common set of utility functions for launching apps. + */ +public class LauncherUtils { + private static final String GMM_PACKAGE_NAME = "com.google.android.apps.maps"; + private static final String GMM_CLASS_NAME = "com.google.android.maps.MapsActivity"; + + public static Intent getLaunchIntent(Context context, String title, String url, String sel) { + Intent intent = null; + String number = parseTelephoneNumber(sel); + if (number != null) { + intent = new Intent(Intent.ACTION_DIAL, + Uri.parse("tel:" + number)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + ClipboardManager cm = + (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + cm.setText(number); + } else if (sel != null && sel.length() > 0) { + // No intent for selection - just copy to clipboard + ClipboardManager cm = + (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + cm.setText(sel); + } else { + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (isMapsURL(url)) { + intent.setClassName(GMM_PACKAGE_NAME, GMM_CLASS_NAME); + } + + // Fall back if we can't resolve intent (i.e. app missing) + PackageManager pm = context.getPackageManager(); + if (null == intent.resolveActivity(pm)) { + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + } + return intent; + } + + public static void generateNotification(Context context, String msg, String title, Intent intent) { + int icon = R.drawable.status_icon; + long when = System.currentTimeMillis(); + + Notification notification = new Notification(icon, title, when); + notification.setLatestEventInfo(context, title, msg, + PendingIntent.getActivity(context, 0, intent, 0)); + notification.flags |= Notification.FLAG_AUTO_CANCEL; + + SharedPreferences settings = Prefs.get(context); + int notificatonID = settings.getInt("notificationID", 0); // allow multiple notifications + + NotificationManager nm = + (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(notificatonID, notification); + playNotificationSound(context); + + SharedPreferences.Editor editor = settings.edit(); + editor.putInt("notificationID", ++notificatonID % 32); + editor.commit(); + } + + public static void playNotificationSound(Context context) { + Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + if (uri != null) { + Ringtone rt = RingtoneManager.getRingtone(context, uri); + if (rt != null) { + rt.setStreamType(AudioManager.STREAM_NOTIFICATION); + rt.play(); + } + } + } + + public static String parseTelephoneNumber(String sel) { + if (sel == null || sel.length() == 0) return null; + + // Hack: Remove trailing left-to-right mark (Google Maps adds this) + if (sel.codePointAt(sel.length() - 1) == 8206) { + sel = sel.substring(0, sel.length() - 1); + } + + String number = null; + if (sel.matches("([Tt]el[:]?)?\\s?[+]?(\\(?[0-9|\\s|\\-|\\.]\\)?)+")) { + String elements[] = sel.split("([Tt]el[:]?)"); + number = elements.length > 1 ? elements[1] : elements[0]; + number = number.replace(" ", ""); + + // Remove option (0) in international numbers, e.g. +44 (0)20 ... + if (number.matches("\\+[0-9]{2,3}\\(0\\).*")) { + int openBracket = number.indexOf('('); + int closeBracket = number.indexOf(')'); + number = number.substring(0, openBracket) + + number.substring(closeBracket + 1); + } + } + return number; + } + + public static boolean isMapsURL(String url) { + return url.matches("http://maps\\.google\\.[a-z]{2,3}(\\.[a-z]{2})?[/?].*") || + url.matches("http://www\\.google\\.[a-z]{2,3}(\\.[a-z]{2})?/maps.*"); + } + + public static boolean isYouTubeURL(String url) { + return url.matches("http://www\\.youtube\\.[a-z]{2,3}(\\.[a-z]{2})?/.*"); + } +} diff --git a/android/src/com/google/android/apps/chrometophone/MainActivity.java b/android/src/com/google/android/apps/chrometophone/SetupActivity.java similarity index 99% rename from android/src/com/google/android/apps/chrometophone/MainActivity.java rename to android/src/com/google/android/apps/chrometophone/SetupActivity.java index b80e9c0..afa5786 100644 --- a/android/src/com/google/android/apps/chrometophone/MainActivity.java +++ b/android/src/com/google/android/apps/chrometophone/SetupActivity.java @@ -45,9 +45,9 @@ import android.widget.RadioGroup.OnCheckedChangeListener; import com.google.android.c2dm.C2DMessaging; /** - * Main activity - takes user through set up. + * Setup activity - takes user through the setup. */ -public class MainActivity extends Activity { +public class SetupActivity extends Activity { public static final String UPDATE_UI_ACTION = "com.google.ctp.UPDATE_UI"; public static final String AUTH_PERMISSION_ACTION = "com.google.ctp.AUTH_PERMISSION"; @@ -95,7 +95,7 @@ public class MainActivity extends Activity { @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.help, menu); + inflater.inflate(R.menu.setup, menu); return true; }