Add history

This commit is contained in:
burke.davey
2010-10-19 07:14:12 +00:00
parent 30124d1a97
commit 3a34fb4109
21 changed files with 816 additions and 165 deletions

View File

@@ -19,9 +19,8 @@
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<application android:icon="@drawable/app_icon" android:label="@string/app_name">
<activity android:name=".MainActivity"
<activity android:name=".HistoryActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -29,7 +28,14 @@
</intent-filter>
</activity>
<activity android:name=".SetupActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="portrait">
</activity>
<activity android:name=".HelpActivity"
android:label="@string/app_name"
android:screenOrientation="portrait">
</activity>

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/background_gradient">
<ImageView
android:id="@+id/header"
android:background="@drawable/header"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dip"/>
<ExpandableListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/header"
android:drawSelectorOnTop="false"
android:layout_margin="5dip"
android:background="@android:color/transparent"
android:cacheColorHint="@android:color/transparent"/>
</RelativeLayout>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:textAppearance="?android:attr/textAppearanceMedium"
android:paddingLeft="35dip"
android:gravity="center_vertical"
android:textColor="#000000"/>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:orientation="horizontal"
android:paddingLeft="6dip"
android:gravity="center_vertical">
<ImageView android:id="@+id/icon"
android:layout_width="20dip"
android:layout_height="20dip"
android:layout_marginLeft="5dip"
android:layout_marginRight="11dip"
android:padding="2dip"/>
<LinearLayout android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView android:id="@+id/title"
android:textAppearance="?android:attr/textAppearanceMedium"
android:maxLines="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:textColor="#000000"/>
<TextView android:id="@+id/url"
android:textAppearance="?android:attr/textAppearanceSmall"
android:maxLines="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:textColor="#000000"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/clear"
android:icon="@android:drawable/ic_menu_close_clear_cancel"
android:title="@string/clear_all"/>
<item android:id="@+id/settings"
android:icon="@android:drawable/ic_menu_preferences"
android:title="@string/settings"/>
<item android:id="@+id/help"
android:icon="@android:drawable/ic_menu_help"
android:title="@string/help"/>
</menu>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="link_action_dialog_items">
<item>Open</item>
<item>Add bookmark</item>
<item>Share link</item>
<item>Copy link url</item>
<item>Remove from history</item>
</string-array>
</resources>

View File

@@ -77,18 +77,6 @@
<!-- Notification shown when text from the desktop is copied to Android's clipboard (user can now paste the text) [CHAR LIMIT=NONE] -->
<string name="copied_desktop_clipboard">Copied desktop clipboard</string>
<!-- Brief notification that appears when user sends link from phone to desktop [CHAR LIMIT=NONE] -->
<string name="sending_link_toast">Sending link...</string>
<!-- Brief notification that appears when user has sent link from phone to desktop [CHAR LIMIT=NONE] -->
<string name="link_sent_toast">Link sent</string>
<!-- Brief notification that appears when link cannot be sent from phone to desktop [CHAR LIMIT=NONE] -->
<string name="link_not_sent_toast">Link not sent</string>
<!-- Brief notification that appears when link cannot be sent from phone to desktop (because of auth issue) [CHAR LIMIT=NONE] -->
<string name="link_not_sent_auth_toast">Link not sent - authentication required</string>
<!-- Introduction text at start of setup flow. Preserve occurrences of &lt;br>, &lt;a href="{tos_link}">, &lt;a href="{pp_link}">, and &lt;a/> [CHAR LIMIT=NONE] -->
<string name="intro_text">
Chrome to Phone lets you easily share links, maps, and currently selected phone numbers and text
@@ -117,4 +105,38 @@
&lt;a href="{tos_link}">Terms of Service&lt;/a> and
&lt;a href="{pp_link}">Privacy Policy&lt;/a>.
</string>
<!-- Label for items received today [CHAR LIMIT=NONE] -->
<string name="today_text">Today</string>
<!-- Label for items received in the last 7 days [CHAR LIMIT=NONE] -->
<string name="last_seven_days_text">Last 7 days</string>
<!-- Label for items received in the last month [CHAR LIMIT=NONE] -->
<string name="last_month_text">Last month</string>
<!-- Label for items received older [CHAR LIMIT=NONE] -->
<string name="older_text">Older</string>
<!-- Menu option for 'Clear all' [CHAR LIMIT=NONE] -->
<string name="clear_all">Clear all</string>
<!-- Menu option for 'Settings' [CHAR LIMIT=NONE] -->
<string name="settings">Settings</string>
<!-- Title for dialog where user is asked to share a link with their choice of app [CHAR LIMIT=NONE] -->
<string name="share_chooser_title">Share with</string>
<!-- Brief notification that appears when user sends link from phone to desktop [CHAR LIMIT=NONE] -->
<string name="sending_link_toast">Sending link...</string>
<!-- Brief notification that appears when user has sent link from phone to desktop [CHAR LIMIT=NONE] -->
<string name="link_sent_toast">Link sent</string>
<!-- Brief notification that appears when link cannot be sent from phone to desktop [CHAR LIMIT=NONE] -->
<string name="link_not_sent_toast">Link not sent</string>
<!-- Brief notification that appears when link cannot be sent from phone to desktop (because of auth issue) [CHAR LIMIT=NONE] -->
<string name="link_not_sent_auth_toast">Link not sent - authentication required</string>
</resources>

View File

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

View File

@@ -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.*");
}
}

View File

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

View File

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

View File

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

View File

@@ -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})?/.*");
}
}

View File

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