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;
}