diff options
40 files changed, 2168 insertions, 752 deletions
diff --git a/NotificationListenerSample/Android.mk b/NotificationListenerSample/Android.mk new file mode 100644 index 0000000..5075a96 --- /dev/null +++ b/NotificationListenerSample/Android.mk @@ -0,0 +1,33 @@ +# +# Copyright (C) 2014 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +LOCAL_AAPT_FLAGS += -c mdpi,hdpi,xhdpi + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 + +LOCAL_PACKAGE_NAME := NotificationListenerSample +LOCAL_CERTIFICATE := platform +include $(BUILD_PACKAGE) + +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/NotificationListenerSample/AndroidManifest.xml b/NotificationListenerSample/AndroidManifest.xml new file mode 100644 index 0000000..e36d422 --- /dev/null +++ b/NotificationListenerSample/AndroidManifest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.example.notificationlistener" + android:versionCode="1" + android:versionName="1.0"> + + <application android:icon="@drawable/icon" android:label="@string/app_name"> + <activity android:name=".NotificationListenerActivity" + android:label="@string/app_name"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + <service android:name="com.android.example.notificationlistener.Listener" + android:label="@string/service_name" + android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> + <intent-filter> + <action android:name="android.service.notification.NotificationListenerService" /> + </intent-filter> + </service> + </application> +</manifest> diff --git a/NotificationListenerSample/res/drawable-hdpi/dialog_ic_dismiss.png b/NotificationListenerSample/res/drawable-hdpi/dialog_ic_dismiss.png Binary files differnew file mode 100644 index 0000000..1aca4b7 --- /dev/null +++ b/NotificationListenerSample/res/drawable-hdpi/dialog_ic_dismiss.png diff --git a/NotificationListenerSample/res/drawable-hdpi/icon.png b/NotificationListenerSample/res/drawable-hdpi/icon.png Binary files differnew file mode 100644 index 0000000..bc976fa --- /dev/null +++ b/NotificationListenerSample/res/drawable-hdpi/icon.png diff --git a/NotificationListenerSample/res/drawable-mdpi/dialog_ic_dismiss.png b/NotificationListenerSample/res/drawable-mdpi/dialog_ic_dismiss.png Binary files differnew file mode 100644 index 0000000..6a2fb5e --- /dev/null +++ b/NotificationListenerSample/res/drawable-mdpi/dialog_ic_dismiss.png diff --git a/NotificationListenerSample/res/drawable-mdpi/icon.png b/NotificationListenerSample/res/drawable-mdpi/icon.png Binary files differnew file mode 100644 index 0000000..e8f7538 --- /dev/null +++ b/NotificationListenerSample/res/drawable-mdpi/icon.png diff --git a/NotificationListenerSample/res/drawable-xhdpi/dialog_ic_dismiss.png b/NotificationListenerSample/res/drawable-xhdpi/dialog_ic_dismiss.png Binary files differnew file mode 100644 index 0000000..e11f2e3 --- /dev/null +++ b/NotificationListenerSample/res/drawable-xhdpi/dialog_ic_dismiss.png diff --git a/NotificationListenerSample/res/drawable-xhdpi/icon.png b/NotificationListenerSample/res/drawable-xhdpi/icon.png Binary files differnew file mode 100644 index 0000000..002515c --- /dev/null +++ b/NotificationListenerSample/res/drawable-xhdpi/icon.png diff --git a/NotificationListenerSample/res/drawable-xxhdpi/dialog_ic_dismiss.png b/NotificationListenerSample/res/drawable-xxhdpi/dialog_ic_dismiss.png Binary files differnew file mode 100644 index 0000000..8d185f4 --- /dev/null +++ b/NotificationListenerSample/res/drawable-xxhdpi/dialog_ic_dismiss.png diff --git a/NotificationListenerSample/res/drawable-xxhdpi/icon.png b/NotificationListenerSample/res/drawable-xxhdpi/icon.png Binary files differnew file mode 100644 index 0000000..e78554f --- /dev/null +++ b/NotificationListenerSample/res/drawable-xxhdpi/icon.png diff --git a/NotificationListenerSample/res/layout/item.xml b/NotificationListenerSample/res/layout/item.xml new file mode 100644 index 0000000..2d9a5ed --- /dev/null +++ b/NotificationListenerSample/res/layout/item.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="match_parent" + android:layout_width="wrap_content" + android:padding="4dp"> + <FrameLayout android:id="@+id/remote_view" + android:layout_height="match_parent" + android:layout_width="wrap_content" + android:onClick="launch" + /> + <ImageButton android:id="@+id/dismiss" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top|end" + android:src="@drawable/dialog_ic_dismiss" + android:onClick="dismiss" + /> +</FrameLayout>
\ No newline at end of file diff --git a/NotificationListenerSample/res/layout/main.xml b/NotificationListenerSample/res/layout/main.xml new file mode 100644 index 0000000..44c9da4 --- /dev/null +++ b/NotificationListenerSample/res/layout/main.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="12dp" + > + <ListView + android:id="@android:id/list" + android:layout_width="match_parent" + android:layout_height="0dp" + android:padding="4dp" + android:layout_marginBottom="16dp" + android:text="@string/working_as_intended" + android:layout_weight="1" + android:choiceMode="singleChoice" + /> + + <TextView android:id="@android:id/empty" + android:layout_width="match_parent" + android:layout_height="0dp" + android:padding="4dp" + android:layout_marginBottom="16dp" + android:text="@string/working_as_intended" + android:layout_weight="1" + /> + <Button + android:id="@+id/launch_settings" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/launch_to_disable" + android:onClick="launchSettings" + android:layout_weight="0" + /> +</LinearLayout> + diff --git a/NotificationListenerSample/res/values/strings.xml b/NotificationListenerSample/res/values/strings.xml new file mode 100644 index 0000000..5c74fda --- /dev/null +++ b/NotificationListenerSample/res/values/strings.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <string name="app_name">NotificationListener</string> + <string name="long_app_name">Notification Listener Sample</string> + <string name="service_name">Sample Notification Listener</string> + <string name="working_as_intended">Service is running. Disable it in Security Settings.</string> + <string name="waiting_for_content">Service is running. Waiting for notifications to arrive.</string> + <string name="nothing_to_see">Service is disabled. You must enabled it to see notifications.</string> + <string name="launch_to_disable">Press to disable service</string> + <string name="launch_to_enable">Press to enable service</string> + <string name="explanation">This app demonstrates use of the Notification Listener Service API. + You must enable the listener service in Security Settings to see it work. + </string> + <string name="disabled">Service is not running.</string> + <string name="enable_it">Enable</string> + <string name="cancel">Cancel</string> +</resources> diff --git a/NotificationListenerSample/src/com/android/example/notificationlistener/Listener.java b/NotificationListenerSample/src/com/android/example/notificationlistener/Listener.java new file mode 100644 index 0000000..466b084 --- /dev/null +++ b/NotificationListenerSample/src/com/android/example/notificationlistener/Listener.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.example.notificationlistener; + + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.Message; +import android.service.notification.NotificationListenerService; +import android.service.notification.NotificationListenerService.Ranking; +import android.service.notification.NotificationListenerService.RankingMap; +import android.service.notification.StatusBarNotification; +import android.support.v4.content.LocalBroadcastManager; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class Listener extends NotificationListenerService { + private static final String TAG = "SampleListener"; + + // Message tags + private static final int MSG_NOTIFY = 1; + private static final int MSG_CANCEL = 2; + private static final int MSG_STARTUP = 3; + private static final int MSG_ORDER = 4; + private static final int MSG_DISMISS = 5; + private static final int MSG_LAUNCH = 6; + private static final int PAGE = 10; + + static final String ACTION_DISMISS = "com.android.example.notificationlistener.DISMISS"; + static final String ACTION_LAUNCH = "com.android.example.notificationlistener.LAUNCH"; + static final String ACTION_REFRESH = "com.android.example.notificationlistener.REFRESH"; + static final String EXTRA_KEY = "key"; + + private static ArrayList<StatusBarNotification> sNotifications; + + public static List<StatusBarNotification> getNotifications() { + return sNotifications; + } + + private final Ranking mTmpRanking = new Ranking(); + + private class Delta { + final StatusBarNotification mSbn; + final RankingMap mRankingMap; + + public Delta(StatusBarNotification sbn, RankingMap rankingMap) { + mSbn = sbn; + mRankingMap = rankingMap; + } + } + + private final Comparator<StatusBarNotification> mRankingComparator = + new Comparator<StatusBarNotification>() { + + private final Ranking mLhsRanking = new Ranking(); + private final Ranking mRhsRanking = new Ranking(); + + @Override + public int compare(StatusBarNotification lhs, StatusBarNotification rhs) { + mRankingMap.getRanking(lhs.getKey(), mLhsRanking); + mRankingMap.getRanking(rhs.getKey(), mRhsRanking); + return Integer.compare(mLhsRanking.getRank(), mRhsRanking.getRank()); + } + }; + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String key = intent.getStringExtra(EXTRA_KEY); + int what = MSG_DISMISS; + if (ACTION_LAUNCH.equals(intent.getAction())) { + what = MSG_LAUNCH; + } + Log.d(TAG, "received an action broadcast " + intent.getAction()); + if (!TextUtils.isEmpty(key)) { + Log.d(TAG, " on " + key); + Message.obtain(mHandler, what, key).sendToTarget(); + } + } + }; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + Delta delta = null; + if (msg.obj instanceof Delta) { + delta = (Delta) msg.obj; + } + + switch (msg.what) { + case MSG_NOTIFY: + Log.i(TAG, "notify: " + delta.mSbn.getKey()); + synchronized (sNotifications) { + boolean exists = mRankingMap.getRanking(delta.mSbn.getKey(), mTmpRanking); + if (!exists) { + sNotifications.add(delta.mSbn); + } else { + int position = mTmpRanking.getRank(); + sNotifications.set(position, delta.mSbn); + } + mRankingMap = delta.mRankingMap; + Collections.sort(sNotifications, mRankingComparator); + Log.i(TAG, "finish with: " + sNotifications.size()); + } + LocalBroadcastManager.getInstance(Listener.this) + .sendBroadcast(new Intent(ACTION_REFRESH) + .putExtra(EXTRA_KEY, delta.mSbn.getKey())); + break; + + case MSG_CANCEL: + Log.i(TAG, "remove: " + delta.mSbn.getKey()); + synchronized (sNotifications) { + boolean exists = mRankingMap.getRanking(delta.mSbn.getKey(), mTmpRanking); + if (exists) { + sNotifications.remove(mTmpRanking.getRank()); + } + mRankingMap = delta.mRankingMap; + Collections.sort(sNotifications, mRankingComparator); + } + LocalBroadcastManager.getInstance(Listener.this) + .sendBroadcast(new Intent(ACTION_REFRESH)); + break; + + case MSG_ORDER: + Log.i(TAG, "reorder"); + synchronized (sNotifications) { + mRankingMap = delta.mRankingMap; + Collections.sort(sNotifications, mRankingComparator); + } + LocalBroadcastManager.getInstance(Listener.this) + .sendBroadcast(new Intent(ACTION_REFRESH)); + break; + + case MSG_STARTUP: + fetchActive(); + Log.i(TAG, "start with: " + sNotifications.size() + " notifications."); + LocalBroadcastManager.getInstance(Listener.this) + .sendBroadcast(new Intent(ACTION_REFRESH)); + break; + + case MSG_DISMISS: + if (msg.obj instanceof String) { + final String key = (String) msg.obj; + mRankingMap.getRanking(key, mTmpRanking); + StatusBarNotification sbn = sNotifications.get(mTmpRanking.getRank()); + if ((sbn.getNotification().flags & Notification.FLAG_AUTO_CANCEL) != 0 && + sbn.getNotification().contentIntent != null) { + try { + sbn.getNotification().contentIntent.send(); + } catch (PendingIntent.CanceledException e) { + Log.d(TAG, "failed to send intent for " + sbn.getKey(), e); + } + } + cancelNotification(key); + } + break; + + case MSG_LAUNCH: + if (msg.obj instanceof String) { + final String key = (String) msg.obj; + mRankingMap.getRanking(key, mTmpRanking); + StatusBarNotification sbn = sNotifications.get(mTmpRanking.getRank()); + if (sbn.getNotification().contentIntent != null) { + try { + sbn.getNotification().contentIntent.send(); + } catch (PendingIntent.CanceledException e) { + Log.d(TAG, "failed to send intent for " + sbn.getKey(), e); + } + } + if ((sbn.getNotification().flags & Notification.FLAG_AUTO_CANCEL) != 0) { + cancelNotification(key); + } + } + break; + } + } + }; + + private RankingMap mRankingMap; + + @Override + public void onCreate() { + super.onCreate(); + Log.d(TAG, "registering broadcast listener"); + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_DISMISS); + intentFilter.addAction(ACTION_LAUNCH); + LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter); + } + + @Override + public void onDestroy() { + LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver); + super.onDestroy(); + } + + @Override + public void onListenerConnected() { + Message.obtain(mHandler, MSG_STARTUP).sendToTarget(); + } + + @Override + public void onNotificationRankingUpdate(RankingMap rankingMap) { + Message.obtain(mHandler, MSG_ORDER, + new Delta(null, rankingMap)).sendToTarget(); + } + + @Override + public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { + Message.obtain(mHandler, MSG_NOTIFY, + new Delta(sbn, rankingMap)).sendToTarget(); + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) { + Message.obtain(mHandler, MSG_CANCEL, + new Delta(sbn, rankingMap)).sendToTarget(); + } + + private void fetchActive() { + mRankingMap = getCurrentRanking(); + sNotifications = new ArrayList<StatusBarNotification>(); + for (StatusBarNotification sbn : getActiveNotifications()) { + sNotifications.add(sbn); + } + Collections.sort(sNotifications, mRankingComparator); + } +} diff --git a/NotificationListenerSample/src/com/android/example/notificationlistener/NotificationListenerActivity.java b/NotificationListenerSample/src/com/android/example/notificationlistener/NotificationListenerActivity.java new file mode 100644 index 0000000..7699d33 --- /dev/null +++ b/NotificationListenerSample/src/com/android/example/notificationlistener/NotificationListenerActivity.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.example.notificationlistener; + +import android.app.AlertDialog; +import android.app.ListActivity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.provider.Settings.Secure; +import android.service.notification.StatusBarNotification; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.TextView; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +public class NotificationListenerActivity extends ListActivity { + private static final String LISTENER_PATH = "com.android.example.notificationlistener/" + + "com.android.example.notificationlistener.Listener"; + private static final String TAG = "NotificationListenerActivity"; + + private Button mLaunchButton; + private TextView mEmptyText; + private StatusAdaptor mStatusAdaptor; + private final BroadcastReceiver mRefreshListener = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "update tickle"); + updateList(intent.getStringExtra(Listener.EXTRA_KEY)); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTitle(R.string.long_app_name); + setContentView(R.layout.main); + mLaunchButton = (Button) findViewById(R.id.launch_settings); + mEmptyText = (TextView) findViewById(android.R.id.empty); + mStatusAdaptor = new StatusAdaptor(this); + setListAdapter(mStatusAdaptor); + } + + @Override + protected void onStop() { + LocalBroadcastManager.getInstance(this).unregisterReceiver(mRefreshListener); + super.onStop(); + } + + @Override + public void onStart() { + super.onStart(); + final IntentFilter intentFilter = new IntentFilter(Listener.ACTION_REFRESH); + LocalBroadcastManager.getInstance(this).registerReceiver(mRefreshListener, intentFilter); + updateList(null); + } + + @Override + public void onResume() { + super.onResume(); + checkEnabled(); + } + + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + checkEnabled(); + } + + private void checkEnabled() { + String listeners = Secure.getString(getContentResolver(), + "enabled_notification_listeners"); + if (listeners != null && listeners.contains(LISTENER_PATH)) { + mLaunchButton.setText(R.string.launch_to_disable); + mEmptyText.setText(R.string.waiting_for_content); + } else { + mLaunchButton.setText(R.string.launch_to_enable); + mEmptyText.setText(R.string.nothing_to_see); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(R.string.explanation) + .setTitle(R.string.disabled); + builder.setPositiveButton(R.string.enable_it, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + launchSettings(null); + } + }); + builder.setNegativeButton(R.string.cancel, null); + builder.create().show(); + } + } + + public void launchSettings(View v) { + startActivityForResult( + new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"), 0); + } + + public void dismiss(View v) { + Log.d(TAG, "clicked dismiss "); + Object tag = v.getTag(); + if (tag instanceof StatusBarNotification) { + StatusBarNotification sbn = (StatusBarNotification) tag; + Log.d(TAG, " on " + sbn.getKey()); + LocalBroadcastManager.getInstance(this). + sendBroadcast(new Intent(Listener.ACTION_DISMISS) + .putExtra(Listener.EXTRA_KEY, sbn.getKey())); + } + } + + public void launch(View v) { + Log.d(TAG, "clicked launch"); + Object tag = v.getTag(); + if (tag instanceof StatusBarNotification) { + StatusBarNotification sbn = (StatusBarNotification) tag; + Log.d(TAG, " on " + sbn.getKey()); + LocalBroadcastManager.getInstance(this). + sendBroadcast(new Intent(Listener.ACTION_LAUNCH) + .putExtra(Listener.EXTRA_KEY, sbn.getKey())); + } + } + + private void updateList(String key) { + if (mStatusAdaptor.requiresInitialization()) { + final List<StatusBarNotification> notifications = Listener.getNotifications(); + if (notifications != null) { + mStatusAdaptor.init(notifications); + } + } + mStatusAdaptor.update(key); + } + + private class StatusAdaptor extends BaseAdapter { + private final Context mContext; + private List<StatusBarNotification> mNotifications; + private HashMap<String, Long> mKeyToId; + private HashSet<String> mKeys; + private long mNextId; + private HashMap<String, View> mRecycledViews; + private String mUpdateKey; + + public StatusAdaptor(Context context) { + mContext = context; + mKeyToId = new HashMap<String, Long>(); + mKeys = new HashSet<String>(); + mNextId = 0; + mRecycledViews = new HashMap<String, View>(); + } + + @Override + public int getCount() { + return mNotifications == null ? 0 : mNotifications.size(); + } + + @Override + public Object getItem(int position) { + return mNotifications.get(position); + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public long getItemId(int position) { + final StatusBarNotification sbn = mNotifications.get(position); + final String key = sbn.getKey(); + if (!mKeyToId.containsKey(key)) { + mKeyToId.put(key, mNextId); + mNextId ++; + } + return mKeyToId.get(key); + } + + @Override + public View getView(int position, View view, ViewGroup list) { + if (view == null) { + view = View.inflate(mContext, R.layout.item, null); + } + FrameLayout container = (FrameLayout) view.findViewById(R.id.remote_view); + View dismiss = view.findViewById(R.id.dismiss); + StatusBarNotification sbn = mNotifications.get(position); + View child; + if (container.getTag() instanceof StatusBarNotification && + container.getChildCount() > 0) { + // recycle the view + StatusBarNotification old = (StatusBarNotification) container.getTag(); + if (sbn.getKey().equals(mUpdateKey)) { + //this view is out of date, discard it + mUpdateKey = null; + } else { + View content = container.getChildAt(0); + container.removeView(content); + mRecycledViews.put(old.getKey(), content); + } + } + child = mRecycledViews.get(sbn.getKey()); + if (child == null) { + child = sbn.getNotification().contentView.apply(mContext, null); + } + container.setTag(sbn); + container.removeAllViews(); + container.addView(child); + dismiss.setVisibility(sbn.isClearable() ? View.VISIBLE : View.GONE); + dismiss.setTag(sbn); + return view; + } + + public void update(String key) { + if (mNotifications != null) { + synchronized (mNotifications) { + mKeys.clear(); + for (int i = 0; i < mNotifications.size(); i++) { + mKeys.add(mNotifications.get(i).getKey()); + } + mKeyToId.keySet().retainAll(mKeys); + } + if (key == null) { + mRecycledViews.clear(); + } else { + mUpdateKey = key; + mRecycledViews.remove(key); + } + Log.d(TAG, "notifyDataSetChanged"); + notifyDataSetChanged(); + } else { + Log.d(TAG, "missed and update"); + } + } + + public boolean requiresInitialization() { + return mNotifications == null; + } + + public void init(List<StatusBarNotification> notifications) { + mNotifications = notifications; + } + } +} diff --git a/NotificationShowcase/AndroidManifest.xml b/NotificationShowcase/AndroidManifest.xml index a36da78..def9061 100644 --- a/NotificationShowcase/AndroidManifest.xml +++ b/NotificationShowcase/AndroidManifest.xml @@ -3,8 +3,12 @@ package="com.android.example.notificationshowcase" android:versionCode="1" android:versionName="1.0"> - <uses-sdk android:minSdkVersion="4" /> + <uses-sdk android:minSdkVersion="4" + android:targetSdkVersion="16" /> + <uses-permission android:name="android.permission.READ_CONTACTS"> + + </uses-permission> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".NotificationShowcaseActivity" android:label="@string/app_name"> @@ -13,7 +17,16 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> - <service android:name=".NotificationShowcaseActivity$UpdateService"/> - <service android:name=".NotificationShowcaseActivity$ProgressService"/> + <activity android:name=".FullScreenActivity" + android:label="@string/full_screen_name"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + </intent-filter> + </activity> + <service android:name=".NotificationService"/> + <service android:name=".UpdateService"/> + <service android:name=".ProgressService"/> + <service android:name=".PhoneService"/> + <service android:name=".ToastService"/> </application> </manifest> diff --git a/NotificationShowcase/res/layout/full_screen.xml b/NotificationShowcase/res/layout/full_screen.xml new file mode 100644 index 0000000..6ff7552 --- /dev/null +++ b/NotificationShowcase/res/layout/full_screen.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ImageView + android:layout_height="match_parent" + android:layout_width="match_parent" + android:src="@drawable/page_hed" + android:onClick="dismiss" + /> +</FrameLayout>
\ No newline at end of file diff --git a/NotificationShowcase/res/values/strings.xml b/NotificationShowcase/res/values/strings.xml index c514e2a..80bf103 100644 --- a/NotificationShowcase/res/values/strings.xml +++ b/NotificationShowcase/res/values/strings.xml @@ -4,4 +4,7 @@ <string name="app_name">NotificationShowcase</string> <string name="post_button_label">Post Notifications</string> <string name="remove_button_label">Remove Notifications</string> + <string name="answered">call answered</string> + <string name="ignored">call ignored</string> + <string name="full_screen_name">Full Screen Activity</string> </resources> diff --git a/NotificationShowcase/src/com/android/example/notificationshowcase/FullScreenActivity.java b/NotificationShowcase/src/com/android/example/notificationshowcase/FullScreenActivity.java new file mode 100644 index 0000000..8286f69 --- /dev/null +++ b/NotificationShowcase/src/com/android/example/notificationshowcase/FullScreenActivity.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.notificationshowcase; + +import android.app.Activity; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; + +public class FullScreenActivity extends Activity { + private static final String TAG = "NotificationShowcase"; + + public static final String EXTRA_ID = "id"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.full_screen); + final Intent intent = getIntent(); + if (intent != null && intent.hasExtra(EXTRA_ID)) { + final int id = intent.getIntExtra(EXTRA_ID, -1); + if (id >= 0) { + NotificationManager noMa = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + noMa.cancel(NotificationService.NOTIFICATION_ID + id); + } + } + } + + public void dismiss(View v) { + finish(); + } + + public static PendingIntent getPendingIntent(Context context, int id) { + Intent fullScreenIntent = new Intent(context, FullScreenActivity.class); + fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + fullScreenIntent.putExtra(EXTRA_ID, id); + PendingIntent pi = PendingIntent.getActivity( + context, 22, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT); + return pi; + } +} diff --git a/NotificationShowcase/src/com/android/example/notificationshowcase/NotificationService.java b/NotificationShowcase/src/com/android/example/notificationshowcase/NotificationService.java new file mode 100644 index 0000000..0b83fa0 --- /dev/null +++ b/NotificationShowcase/src/com/android/example/notificationshowcase/NotificationService.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.notificationshowcase; + +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Typeface; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.provider.ContactsContract; +import android.support.v4.app.NotificationCompat; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.StyleSpan; +import android.util.Log; +import android.view.View; + +import java.util.ArrayList; + +public class NotificationService extends IntentService { + + private static final String TAG = "NotificationService"; + + public static final String ACTION_CREATE = "create"; + public static final int NOTIFICATION_ID = 31338; + + public NotificationService() { + super(TAG); + } + + public NotificationService(String name) { + super(name); + } + + private static Bitmap getBitmap(Context context, int resId) { + int largeIconWidth = (int) context.getResources() + .getDimension(R.dimen.notification_large_icon_width); + int largeIconHeight = (int) context.getResources() + .getDimension(R.dimen.notification_large_icon_height); + Drawable d = context.getResources().getDrawable(resId); + Bitmap b = Bitmap.createBitmap(largeIconWidth, largeIconHeight, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + d.setBounds(0, 0, largeIconWidth, largeIconHeight); + d.draw(c); + return b; + } + + private static PendingIntent makeEmailIntent(Context context, String who) { + final Intent intent = new Intent(android.content.Intent.ACTION_SENDTO, + Uri.parse("mailto:" + who)); + return PendingIntent.getActivity( + context, 0, intent, + PendingIntent.FLAG_CANCEL_CURRENT); + } + + public static Notification makeBigTextNotification(Context context, int update, int id, + long when) { + String personUri = null; + Cursor c = null; + try { + String[] projection = new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.LOOKUP_KEY }; + String selections = ContactsContract.Contacts.DISPLAY_NAME + " = 'Mike Cleron'"; + final ContentResolver contentResolver = context.getContentResolver(); + c = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, + projection, selections, null, null); + if (c != null && c.getCount() > 0) { + c.moveToFirst(); + int lookupIdx = c.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY); + int idIdx = c.getColumnIndex(ContactsContract.Contacts._ID); + String lookupKey = c.getString(lookupIdx); + long contactId = c.getLong(idIdx); + Uri lookupUri = ContactsContract.Contacts.getLookupUri(contactId, lookupKey); + personUri = lookupUri.toString(); + } + } finally { + if (c != null) { + c.close(); + } + } + if (TextUtils.isEmpty(personUri)) { + Log.w(TAG, "failed to find contact for Mike Cleron"); + } else { + Log.w(TAG, "Mike Cleron is " + personUri); + } + + String addendum = update > 0 ? "(updated) " : ""; + String longSmsText = "Hey, looks like\nI'm getting kicked out of this conference" + + " room"; + if (update > 1) { + longSmsText += ", so stay in the hangout and I'll rejoin in about 5-10 minutes" + + ". If you don't see me, assume I got pulled into another meeting. And" + + " now \u2026 I have to find my shoes."; + } + if (update > 2) { + when = System.currentTimeMillis(); + } + NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle(); + bigTextStyle.bigText(addendum + longSmsText); + Notification bigText = new NotificationCompat.Builder(context) + .setContentTitle(addendum + "Mike Cleron") + .setContentIntent(ToastService.getPendingIntent(context, "Clicked on bigText")) + .setContentText(addendum + longSmsText) + .setTicker(addendum + "Mike Cleron: " + longSmsText) + .setWhen(when) + .setLargeIcon(getBitmap(context, R.drawable.bucket)) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .addAction(R.drawable.ic_media_next, + "update: " + update, + UpdateService.getPendingIntent(context, update + 1, id, when)) + .setSmallIcon(R.drawable.stat_notify_talk_text) + .setStyle(bigTextStyle) + .setDefaults(Notification.DEFAULT_SOUND) + .addPerson(personUri) + .build(); + return bigText; + } + + public static Notification makeUploadNotification(Context context, int progress, long when) { + NotificationCompat.Builder uploadNotification = new NotificationCompat.Builder(context) + .setContentTitle("File Upload") + .setContentText("foo.txt") + .setPriority(NotificationCompat.PRIORITY_MIN) + .setContentIntent(ToastService.getPendingIntent(context, "Clicked on Upload")) + .setWhen(when) + .setSmallIcon(R.drawable.ic_menu_upload) + .setProgress(100, Math.min(progress, 100), false); + return uploadNotification.build(); + } + + @Override + protected void onHandleIntent(Intent intent) { + ArrayList<Notification> mNotifications = new ArrayList<Notification>(); + NotificationManager noMa = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + int bigtextId = mNotifications.size(); + mNotifications.add(makeBigTextNotification(this, 0, bigtextId, System.currentTimeMillis())); + + int uploadId = mNotifications.size(); + long uploadWhen = System.currentTimeMillis(); + mNotifications.add(makeUploadNotification(this, 10, uploadWhen)); + + int phoneId = mNotifications.size(); + final PendingIntent fullscreenIntent = FullScreenActivity.getPendingIntent(this, phoneId); + Notification phoneCall = new NotificationCompat.Builder(this) + .setContentTitle("Incoming call") + .setContentText("Matias Duarte") + .setLargeIcon(getBitmap(this, R.drawable.matias_hed)) + .setSmallIcon(R.drawable.stat_sys_phone_call) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setContentIntent(fullscreenIntent) + .setFullScreenIntent(fullscreenIntent, true) + .addAction(R.drawable.ic_dial_action_call, "Answer", + PhoneService.getPendingIntent(this, phoneId, PhoneService.ACTION_ANSWER)) + .addAction(R.drawable.ic_end_call, "Ignore", + PhoneService.getPendingIntent(this, phoneId, PhoneService.ACTION_IGNORE)) + .setOngoing(true) + .addPerson(Uri.fromParts("tel", "1 (617) 555-1212", null).toString()) + .build(); + mNotifications.add(phoneCall); + + mNotifications.add(new NotificationCompat.Builder(this) + .setContentTitle("Stopwatch PRO") + .setContentText("Counting up") + .setContentIntent(ToastService.getPendingIntent(this, "Clicked on Stopwatch")) + .setSmallIcon(R.drawable.stat_notify_alarm) + .setUsesChronometer(true) + .build()); + + mNotifications.add(new NotificationCompat.Builder(this) + .setContentTitle("J Planning") + .setContentText("The Botcave") + .setWhen(System.currentTimeMillis()) + .setSmallIcon(R.drawable.stat_notify_calendar) + .setContentIntent(ToastService.getPendingIntent(this, "Clicked on calendar event")) + .setContentInfo("7PM") + .addAction(R.drawable.stat_notify_snooze, "+10 min", + ToastService.getPendingIntent(this, "snoozed 10 min")) + .addAction(R.drawable.stat_notify_snooze_longer, "+1 hour", + ToastService.getPendingIntent(this, "snoozed 1 hr")) + .addAction(R.drawable.stat_notify_email, "Email", + makeEmailIntent(this, + "gabec@example.com,mcleron@example.com,dsandler@example.com")) + .build()); + + BitmapDrawable d = + (BitmapDrawable) getResources().getDrawable(R.drawable.romainguy_rockaway); + mNotifications.add(new NotificationCompat.BigPictureStyle( + new NotificationCompat.Builder(this) + .setContentTitle("Romain Guy") + .setContentText("I was lucky to find a Canon 5D Mk III at a local Bay Area " + + "store last week but I had not been able to try it in the field " + + "until tonight. After a few days of rain the sky finally cleared " + + "up. Rockaway Beach did not disappoint and I was finally able to " + + "see what my new camera feels like when shooting landscapes.") + .setSmallIcon(R.drawable.ic_stat_gplus) + .setContentIntent( + ToastService.getPendingIntent(this, "Clicked on bigPicture")) + .setLargeIcon(getBitmap(this, R.drawable.romainguy_hed)) + .addAction(R.drawable.add, "Add to Gallery", + ToastService.getPendingIntent(this, "added! (just kidding)")) + .setSubText("talk rocks!")) + .bigPicture(d.getBitmap()) + .build()); + + // Note: this may conflict with real email notifications + StyleSpan bold = new StyleSpan(Typeface.BOLD); + SpannableString line1 = new SpannableString("Alice: hey there!"); + line1.setSpan(bold, 0, 5, 0); + SpannableString line2 = new SpannableString("Bob: hi there!"); + line2.setSpan(bold, 0, 3, 0); + SpannableString line3 = new SpannableString("Charlie: Iz IN UR EMAILZ!!"); + line3.setSpan(bold, 0, 7, 0); + mNotifications.add(new NotificationCompat.InboxStyle( + new NotificationCompat.Builder(this) + .setContentTitle("24 new messages") + .setContentText("You have mail!") + .setSubText("test.hugo2@gmail.com") + .setContentIntent(ToastService.getPendingIntent(this, "Clicked on Email")) + .setSmallIcon(R.drawable.stat_notify_email)) + .setSummaryText("+21 more") + .addLine(line1) + .addLine(line2) + .addLine(line3) + .build()); + + mNotifications.add(new NotificationCompat.Builder(this) + .setContentTitle("Twitter") + .setContentText("New mentions") + .setContentIntent(ToastService.getPendingIntent(this, "Clicked on Twitter")) + .setSmallIcon(R.drawable.twitter_icon) + .setNumber(15) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build()); + + + for (int i=0; i<mNotifications.size(); i++) { + noMa.notify(NOTIFICATION_ID + i, mNotifications.get(i)); + } + + ProgressService.startProgressUpdater(this, uploadId, uploadWhen, 0); + } +} diff --git a/NotificationShowcase/src/com/android/example/notificationshowcase/NotificationShowcaseActivity.java b/NotificationShowcase/src/com/android/example/notificationshowcase/NotificationShowcaseActivity.java index ee060ce..1e0fa20 100644 --- a/NotificationShowcase/src/com/android/example/notificationshowcase/NotificationShowcaseActivity.java +++ b/NotificationShowcase/src/com/android/example/notificationshowcase/NotificationShowcaseActivity.java @@ -3,372 +3,20 @@ package com.android.example.notificationshowcase; -import java.util.ArrayList; import android.app.Activity; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; import android.content.ComponentName; -import android.content.Context; import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Typeface; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Binder; import android.os.Bundle; -import android.os.IBinder; -import android.support.v4.app.NotificationCompat; -import android.text.SpannableString; -import android.text.style.StyleSpan; -import android.util.Log; -import android.view.View; -import android.widget.Toast; public class NotificationShowcaseActivity extends Activity { private static final String TAG = "NotificationShowcase"; - - private static final int NOTIFICATION_ID = 31338; - - private static int bigtextId; - private static int uploadId; - - private static final boolean FIRE_AND_FORGET = true; - - public static class ToastFeedbackActivity extends Activity { - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - } - - @Override - public void onResume() { - super.onResume(); - Intent i = getIntent(); - Log.v(TAG, "clicked a thing! intent=" + i.toString()); - if (i.hasExtra("text")) { - final String text = i.getStringExtra("text"); - Toast.makeText(this, text, Toast.LENGTH_LONG).show(); - } - finish(); - } - } - - public static class UpdateService extends Service { - @Override - public IBinder onBind(Intent intent) { - Log.v(TAG, "onbind"); - return null; - } - - @Override - public void onStart(Intent i, int startId) { - super.onStart(i, startId); - try { - // allow the user close the shade, if they want to test that. - Thread.sleep(3000); - } catch (Exception e) { - } - Log.v(TAG, "clicked a thing! intent=" + i.toString()); - if (i.hasExtra("id") && i.hasExtra("when")) { - final int id = i.getIntExtra("id", 0); - if (id == bigtextId) { - NotificationManager noMa = - (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - final int update = i.getIntExtra("update", 0); - final long when = i.getLongExtra("when", 0L); - Log.v(TAG, "id: " + id + " when: " + when + " update: " + update); - noMa.notify(NOTIFICATION_ID + id, - makeBigTextNotification(this, update, id, when)); - } - } else { - Log.v(TAG, "id extra was " + (i.hasExtra("id") ? "present" : "missing")); - Log.v(TAG, "when extra was " + (i.hasExtra("when") ? "present" : "missing")); - } - } - } - - public static class ProgressService extends Service { - @Override - public IBinder onBind(Intent intent) { - Log.v(TAG, "onbind"); - return null; - } - - @Override - public void onStart(Intent i, int startId) { - super.onStart(i, startId); - if (i.hasExtra("id") && i.hasExtra("when") && i.hasExtra("progress")) { - final int id = i.getIntExtra("id", 0); - if (id == uploadId) { - final long when = i.getLongExtra("when", 0L); - int progress = i.getIntExtra("progress", 0); - NotificationManager noMa = (NotificationManager) - getSystemService(Context.NOTIFICATION_SERVICE); - while (progress <= 100) { - try { - // allow the user close the shade, if they want to test that. - Thread.sleep(1000); - } catch (Exception e) { - } - Log.v(TAG, "id: " + id + " when: " + when + " progress: " + progress); - noMa.notify(NOTIFICATION_ID + id, - makeUploadNotification(this, progress, id, when)); - progress+=10; - } - } - } else { - Log.v(TAG, "id extra " + (i.hasExtra("id") ? "present" : "missing")); - Log.v(TAG, "when extra " + (i.hasExtra("when") ? "present" : "missing")); - Log.v(TAG, "progress extra " + (i.hasExtra("progress") ? "present" : "missing")); - } - } - } - - private ArrayList<Notification> mNotifications = new ArrayList<Notification>(); - NotificationManager mNoMa; - - static int mLargeIconWidth, mLargeIconHeight; - private static Bitmap getBitmap(Context context, int resId) { - Drawable d = context.getResources().getDrawable(resId); - Bitmap b = Bitmap.createBitmap(mLargeIconWidth, mLargeIconHeight, Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(b); - d.setBounds(0, 0, mLargeIconWidth, mLargeIconHeight); - d.draw(c); - return b; - } - - private static PendingIntent makeToastIntent(Context context, String s) { - Intent toastIntent = new Intent(context, ToastFeedbackActivity.class); - toastIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - toastIntent.putExtra("text", s); - PendingIntent pi = PendingIntent.getActivity( - context, 58, toastIntent, PendingIntent.FLAG_CANCEL_CURRENT); - return pi; - } - - private static PendingIntent makeEmailIntent(Context context, String who) { - final Intent intent = new Intent(android.content.Intent.ACTION_SENDTO, - Uri.parse("mailto:" + who)); - return PendingIntent.getActivity( - context, 0, intent, - PendingIntent.FLAG_CANCEL_CURRENT); - } - - // this is a service, it will only close the notification shade if used as a contentIntent. - private static int updateId = 3000; - private static PendingIntent makeUpdateIntent(Context context, int update, int id, long when) { - Intent updateIntent = new Intent(); - updateIntent.setComponent( - new ComponentName(context, UpdateService.class)); - updateIntent.putExtra("id", id); - updateIntent.putExtra("when", when); - updateIntent.putExtra("update", update); - Log.v(TAG, "added id extra " + id); - Log.v(TAG, "added when extra " + when); - PendingIntent pi = PendingIntent.getService( - context, updateId++, updateIntent, PendingIntent.FLAG_UPDATE_CURRENT); - return pi; - } - - private static Notification makeBigTextNotification(Context context, int update, int id, - long when) { - String addendum = update > 0 ? "(updated) " : ""; - String longSmsText = "Hey, looks like\nI'm getting kicked out of this conference" + - " room"; - if (update > 1) { - longSmsText += ", so stay in the hangout and I'll rejoin in about 5-10 minutes" + - ". If you don't see me, assume I got pulled into another meeting. And" + - " now \u2026 I have to find my shoes. Four score and seven years "+ - "ago our fathers brought forth on this continent, a new nation, conceived "+ - "in Liberty, and dedicated to the proposition that all men are created "+ - "equal. Now we are engaged in a great civil war, testing whether that "+ - "nation, or any nation so conceived and so dedicated, can long "+ - "endure. We are met on a great battle-field of that war. We have come "+ - "to dedicate a portion of that field, as a final resting place for "+ - "those who here gave their lives that that nation might live. It is "+ - "altogether fitting and proper that we should do this.But, in a larger "+ - "sense, we can not dedicate -- we can not consecrate -- we can not "+ - "hallow -- this ground.The brave men, living and dead, who struggled "+ - "here, have consecrated it, far above our poor power to add or detract."+ - " The world will little note, nor long remember what we say here, but "+ - "it can never forget what they did here. It is for us the living, rather,"+ - " to be dedicated here to the unfinished work which they who fought "+ - "here have thus far so nobly advanced.It is rather for us to be here "+ - "dedicated to the great task remaining before us -- that from these "+ - "honored dead we take increased devotion to that cause for which they "+ - "gave the last full measure of devotion -- that we here highly resolve "+ - "that these dead shall not have died in vain -- that this nation, under "+ - "God, shall have a new birth of freedom -- and that government of "+ - "the people, by the people, for the people, shall not perish from the earth."; - } - if (update > 2) { - when = System.currentTimeMillis(); - } - NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle(); - bigTextStyle.bigText(addendum + longSmsText); - NotificationCompat.Builder bigTextNotification = new NotificationCompat.Builder(context) - .setContentTitle(addendum + "Mike Cleron") - .setContentIntent(makeToastIntent(context, "Clicked on bigText")) - .setContentText(addendum + longSmsText) - .setTicker(addendum + "Mike Cleron: " + longSmsText) - .setWhen(when) - .setLargeIcon(getBitmap(context, R.drawable.bucket)) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .addAction(R.drawable.ic_media_next, - "update: " + update, - makeUpdateIntent(context, update+1, id, when)) - .setSmallIcon(R.drawable.stat_notify_talk_text) - .setStyle(bigTextStyle); - return bigTextNotification.build(); - } - - // this is a service, it will only close the notification shade if used as a contentIntent. - private static void startProgressUpdater(Context context, int progress, int id, long when) { - Intent progressIntent = new Intent(); - progressIntent.setComponent(new ComponentName(context, ProgressService.class)); - progressIntent.putExtra("id", id); - progressIntent.putExtra("when", when); - progressIntent.putExtra("progress", progress); - context.startService(progressIntent); - } - - private static Notification makeUploadNotification(Context context, int progress, int id, - long when) { - NotificationCompat.Builder uploadNotification = new NotificationCompat.Builder(context) - .setContentTitle("File Upload") - .setContentText("foo.txt") - .setPriority(NotificationCompat.PRIORITY_MIN) - .setContentIntent(makeToastIntent(context, "Clicked on Upload")) - .setWhen(when) - .setSmallIcon(R.drawable.ic_menu_upload) - .setProgress(100, Math.min(progress, 100), false); - return uploadNotification.build(); - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); - - mLargeIconWidth = (int) getResources().getDimension(R.dimen.notification_large_icon_width); - mLargeIconHeight = (int) getResources().getDimension(R.dimen.notification_large_icon_height); - - mNoMa = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - - bigtextId = mNotifications.size(); - mNotifications.add(makeBigTextNotification(this, 0, bigtextId, - System.currentTimeMillis())); - - uploadId = mNotifications.size(); - long uploadWhen = System.currentTimeMillis(); - mNotifications.add(makeUploadNotification(this, 10, uploadId, uploadWhen)); - - mNotifications.add(new NotificationCompat.Builder(this) - .setContentTitle("Incoming call") - .setContentText("Matias Duarte") - .setLargeIcon(getBitmap(this, R.drawable.matias_hed)) - .setSmallIcon(R.drawable.stat_sys_phone_call) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setContentIntent(makeToastIntent(this, "Clicked on Matias")) - .addAction(R.drawable.ic_dial_action_call, "Answer", makeToastIntent(this, "call answered")) - .addAction(R.drawable.ic_end_call, "Ignore", makeToastIntent(this, "call ignored")) - .setAutoCancel(true) - .build()); - - mNotifications.add(new NotificationCompat.Builder(this) - .setContentTitle("Stopwatch PRO") - .setContentText("Counting up") - .setContentIntent(makeToastIntent(this, "Clicked on Stopwatch")) - .setSmallIcon(R.drawable.stat_notify_alarm) - .setUsesChronometer(true) - .build()); - - mNotifications.add(new NotificationCompat.Builder(this) - .setContentTitle("J Planning") - .setContentText("The Botcave") - .setWhen(System.currentTimeMillis()) - .setSmallIcon(R.drawable.stat_notify_calendar) - .setContentIntent(makeToastIntent(this, "tapped in the calendar event")) - .setContentInfo("7PM") - .addAction(R.drawable.stat_notify_snooze, "+10 min", - makeToastIntent(this, "snoozed 10 min")) - .addAction(R.drawable.stat_notify_snooze_longer, "+1 hour", - makeToastIntent(this, "snoozed 1 hr")) - .addAction(R.drawable.stat_notify_email, "Email", - makeEmailIntent(this, - "gabec@example.com,mcleron@example.com,dsandler@example.com")) - .build()); - - BitmapDrawable d = - (BitmapDrawable) getResources().getDrawable(R.drawable.romainguy_rockaway); - mNotifications.add(new NotificationCompat.BigPictureStyle( - new NotificationCompat.Builder(this) - .setContentTitle("Romain Guy") - .setContentText("I was lucky to find a Canon 5D Mk III at a local Bay Area store last " - + "week but I had not been able to try it in the field until tonight. After a " - + "few days of rain the sky finally cleared up. Rockaway Beach did not disappoint " - + "and I was finally able to see what my new camera feels like when shooting " - + "landscapes.") - .setSmallIcon(R.drawable.ic_stat_gplus) - .setContentIntent(makeToastIntent(this, "Clicked on bigPicture")) - .setLargeIcon(getBitmap(this, R.drawable.romainguy_hed)) - .addAction(R.drawable.add, "Add to Gallery", - makeToastIntent(this, "added! (just kidding)")) - .setSubText("talk rocks!") - ) - .bigPicture(d.getBitmap()) - .build()); - - // Note: this may conflict with real email notifications - StyleSpan bold = new StyleSpan(Typeface.BOLD); - SpannableString line1 = new SpannableString("Alice: hey there!"); - line1.setSpan(bold, 0, 5, 0); - SpannableString line2 = new SpannableString("Bob: hi there!"); - line2.setSpan(bold, 0, 3, 0); - SpannableString line3 = new SpannableString("Charlie: Iz IN UR EMAILZ!!"); - line3.setSpan(bold, 0, 7, 0); - mNotifications.add(new NotificationCompat.InboxStyle( - new NotificationCompat.Builder(this) - .setContentTitle("24 new messages") - .setContentText("You have mail!") - .setSubText("test.hugo2@gmail.com") - .setContentIntent(makeToastIntent(this, "Clicked on Email")) - .setSmallIcon(R.drawable.stat_notify_email)) - .setSummaryText("+21 more") - .addLine(line1) - .addLine(line2) - .addLine(line3) - .build()); - - mNotifications.add(new NotificationCompat.Builder(this) - .setContentTitle("Twitter") - .setContentText("New mentions") - .setContentIntent(makeToastIntent(this, "Clicked on Twitter")) - .setSmallIcon(R.drawable.twitter_icon) - .setNumber(15) - .setPriority(NotificationCompat.PRIORITY_LOW) - .build()); - - if (FIRE_AND_FORGET) { - doPost(null); - startProgressUpdater(this, 10, uploadId, uploadWhen); - finish(); - } - } - - public void doPost(View v) { - for (int i=0; i<mNotifications.size(); i++) { - mNoMa.notify(NOTIFICATION_ID + i, mNotifications.get(i)); - } - } - - public void doRemove(View v) { - for (int i=0; i<mNotifications.size(); i++) { - mNoMa.cancel(NOTIFICATION_ID + i); - } + Intent intent = new Intent(NotificationService.ACTION_CREATE); + intent.setComponent(new ComponentName(this, NotificationService.class)); + startService(intent); + finish(); } } diff --git a/NotificationShowcase/src/com/android/example/notificationshowcase/PhoneService.java b/NotificationShowcase/src/com/android/example/notificationshowcase/PhoneService.java new file mode 100644 index 0000000..0b4f3bd --- /dev/null +++ b/NotificationShowcase/src/com/android/example/notificationshowcase/PhoneService.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.notificationshowcase; + +import android.app.IntentService; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.util.Log; +import android.widget.Toast; + +public class PhoneService extends IntentService { + + private static final String TAG = "PhoneService"; + + public static final String ACTION_ANSWER = "answer"; + public static final String ACTION_IGNORE = "ignore"; + + public static final String EXTRA_ID = "id"; + + private Handler handler; + + public PhoneService() { + super(TAG); + } + public PhoneService(String name) { + super(name); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + handler = new Handler(); + return super.onStartCommand(intent, flags, startId); + } + + @Override + protected void onHandleIntent(Intent intent) { + Log.v(TAG, "clicked a thing! intent=" + intent.toString()); + int res = ACTION_ANSWER.equals(intent.getAction()) ? R.string.answered : R.string.ignored; + final String text = getString(res); + final int id = intent.getIntExtra(EXTRA_ID, -1); + handler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(PhoneService.this, text, Toast.LENGTH_LONG).show(); + if (id >= 0) { + NotificationManager noMa = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + noMa.cancel(NotificationService.NOTIFICATION_ID + id); + } + Log.v(TAG, "phone toast " + text); + } + }); + } + + public static PendingIntent getPendingIntent(Context context, int id, String action) { + Intent phoneIntent = new Intent(context, PhoneService.class); + phoneIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + phoneIntent.setAction(action); + phoneIntent.putExtra(EXTRA_ID, id); + PendingIntent pi = PendingIntent.getService( + context, 58, phoneIntent, PendingIntent.FLAG_UPDATE_CURRENT); + return pi; + } +} diff --git a/NotificationShowcase/src/com/android/example/notificationshowcase/ProgressService.java b/NotificationShowcase/src/com/android/example/notificationshowcase/ProgressService.java new file mode 100644 index 0000000..799708d --- /dev/null +++ b/NotificationShowcase/src/com/android/example/notificationshowcase/ProgressService.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.notificationshowcase; + +import android.app.IntentService; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.util.Log; + +public class ProgressService extends IntentService { + + private static final String TAG = "ProgressService"; + + private static final String ACTION_PROGRESS = "progress"; + + private Handler handler; + + public ProgressService() { + super(TAG); + } + public ProgressService(String name) { + super(name); + } + + class UpdateRunnable implements Runnable { + + private final int mId; + private final long mWhen; + private int mProgress; + + UpdateRunnable(int id, long when, int progress) { + mId = id; + mWhen = when; + mProgress = progress; + } + + @Override + public void run() { NotificationManager noMa = (NotificationManager) + getSystemService(Context.NOTIFICATION_SERVICE); + Log.v(TAG, "id: " + mId + " when: " + mWhen + " progress: " + mProgress); + noMa.notify(NotificationService.NOTIFICATION_ID + mId, + NotificationService.makeUploadNotification( + ProgressService.this, mProgress, mWhen)); + mProgress += 10; + if (mProgress <= 100) { + handler.postDelayed(this, 1000); + } + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + handler = new Handler(); + return super.onStartCommand(intent, flags, startId); + } + + + @Override + protected void onHandleIntent(Intent intent) { + final int id = intent.getIntExtra("id", 0); + final long when = intent.getLongExtra("when", 0L); + int progress = intent.getIntExtra("progress", 0); + handler.postDelayed(new UpdateRunnable(id, when, progress), 1000); + } + + public static void startProgressUpdater(Context context, int id, long when, int progress) { + Intent progressIntent = new Intent(context, ProgressService.class); + progressIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + progressIntent.setAction(ACTION_PROGRESS); + progressIntent.putExtra("id", id); + progressIntent.putExtra("when", when); + progressIntent.putExtra("progress", progress); + context.startService(progressIntent); + } +} diff --git a/NotificationShowcase/src/com/android/example/notificationshowcase/ToastService.java b/NotificationShowcase/src/com/android/example/notificationshowcase/ToastService.java new file mode 100644 index 0000000..6ecfeb2 --- /dev/null +++ b/NotificationShowcase/src/com/android/example/notificationshowcase/ToastService.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.notificationshowcase; + +import android.app.IntentService; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.util.Log; +import android.widget.Toast; + +public class ToastService extends IntentService { + + private static final String TAG = "ToastService"; + + private static final String ACTION_TOAST = "toast"; + + private Handler handler; + + public ToastService() { + super(TAG); + } + public ToastService(String name) { + super(name); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + handler = new Handler(); + return super.onStartCommand(intent, flags, startId); + } + + @Override + protected void onHandleIntent(Intent intent) { + Log.v(TAG, "clicked a thing! intent=" + intent.toString()); + if (intent.hasExtra("text")) { + final String text = intent.getStringExtra("text"); + handler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(ToastService.this, text, Toast.LENGTH_LONG).show(); + Log.v(TAG, "toast " + text); + } + }); + } + } + + public static PendingIntent getPendingIntent(Context context, String text) { + Intent toastIntent = new Intent(context, ToastService.class); + toastIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + toastIntent.setAction(ACTION_TOAST + ":" + text); // one per toast message + toastIntent.putExtra("text", text); + PendingIntent pi = PendingIntent.getService( + context, 58, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT); + return pi; + } +} diff --git a/NotificationShowcase/src/com/android/example/notificationshowcase/UpdateService.java b/NotificationShowcase/src/com/android/example/notificationshowcase/UpdateService.java new file mode 100644 index 0000000..23b8de5 --- /dev/null +++ b/NotificationShowcase/src/com/android/example/notificationshowcase/UpdateService.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.notificationshowcase; + +import android.app.IntentService; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class UpdateService extends IntentService { + + private static final String TAG = "UpdateService"; + + private static final String ACTION_UPDATE = "update"; + + public UpdateService() { + super(TAG); + } + + public UpdateService(String name) { + super(name); + } + + @Override + protected void onHandleIntent(Intent intent) { + + try { + // allow the user close the shade, if they want to test that. + Thread.sleep(3000); + } catch (Exception e) { + } + Log.v(TAG, "clicked a thing! intent=" + intent.toString()); + if (intent.hasExtra("id") && intent.hasExtra("when")) { + final int id = intent.getIntExtra("id", 0); + NotificationManager noMa = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + final int update = intent.getIntExtra("update", 0); + final long when = intent.getLongExtra("when", 0L); + Log.v(TAG, "id: " + id + " when: " + when + " update: " + update); + noMa.notify(NotificationService.NOTIFICATION_ID + id, + NotificationService.makeBigTextNotification(this, update, id, when)); + } else { + Log.v(TAG, "id extra was " + (intent.hasExtra("id") ? "present" : "missing")); + Log.v(TAG, "when extra was " + (intent.hasExtra("when") ? "present" : "missing")); + } + } + + public static PendingIntent getPendingIntent(Context context, int update, int id, long when) { + Intent updateIntent = new Intent(context, UpdateService.class); + updateIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + updateIntent.setAction(ACTION_UPDATE); + updateIntent.putExtra("id", id); + updateIntent.putExtra("when", when); + updateIntent.putExtra("update", update); + PendingIntent pi = PendingIntent.getService( + context, 58, updateIntent, PendingIntent.FLAG_UPDATE_CURRENT); + return pi; + } +} diff --git a/PrintApp/Android.mk b/PrintApp/Android.mk index 7cea0ab..5e1c4fa 100644 --- a/PrintApp/Android.mk +++ b/PrintApp/Android.mk @@ -8,6 +8,4 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_PACKAGE_NAME := Print -LOCAL_CERTIFICATE := platform - include $(BUILD_PACKAGE) diff --git a/PrintApp/AndroidManifest.xml b/PrintApp/AndroidManifest.xml index 7528f28..1442178 100644 --- a/PrintApp/AndroidManifest.xml +++ b/PrintApp/AndroidManifest.xml @@ -4,8 +4,8 @@ android:versionName="1.0" > <uses-sdk - android:minSdkVersion="KeyLimePie" - android:targetSdkVersion="KeyLimePie" /> + android:minSdkVersion="19" + android:targetSdkVersion="19" /> <application android:icon="@drawable/ic_launcher" diff --git a/PrintApp/res/menu/activity_main.xml b/PrintApp/res/menu/activity_main.xml index 31bb883..060c09a 100644 --- a/PrintApp/res/menu/activity_main.xml +++ b/PrintApp/res/menu/activity_main.xml @@ -1,8 +1,4 @@ <menu xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:id="@+id/menu_settings" - android:title="@string/menu_settings" - android:orderInCategory="100" - android:showAsAction="never" /> <item android:id="@+id/menu_print" android:title="@string/print" android:orderInCategory="100" diff --git a/PrintApp/src/foo/bar/print/PrintActivity.java b/PrintApp/src/foo/bar/print/PrintActivity.java index dfd339b..03c3b89 100644 --- a/PrintApp/src/foo/bar/print/PrintActivity.java +++ b/PrintApp/src/foo/bar/print/PrintActivity.java @@ -16,31 +16,30 @@ package foo.bar.print; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + import android.app.Activity; import android.content.Context; +import android.graphics.pdf.PdfDocument.Page; import android.os.AsyncTask; import android.os.Bundle; import android.os.CancellationSignal; import android.os.CancellationSignal.OnCancelListener; +import android.os.ParcelFileDescriptor; import android.print.PageRange; import android.print.PrintAttributes; import android.print.PrintDocumentAdapter; import android.print.PrintDocumentInfo; -import android.print.PrintJob; import android.print.PrintManager; -import android.print.pdf.PdfDocument.Page; -import android.util.Log; +import android.print.pdf.PrintedPdfDocument; +import android.util.SparseIntArray; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileOutputStream; -import java.util.List; - -import libcore.io.IoUtils; - /** * Simple sample of how to use the print APIs. */ @@ -48,6 +47,8 @@ public class PrintActivity extends Activity { public static final String LOG_TAG = "PrintActivity"; + private static final int PAGE_COUNT = 5; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -63,106 +64,213 @@ public class PrintActivity extends Activity { @Override public boolean onOptionsItemSelected(MenuItem item) { - if (!super.onOptionsItemSelected(item)) { - if (item.getItemId() == R.id.menu_print) { - printView(); - return true; - } - } - return false; - } - - public void printFileSimple() { - PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); - - PrintJob printJob = printManager.print("My Print Job", new File("foo.pdf"), null); - - if (printJob != null) { - /* Yay, we scheduled something to be printed!!! */ + if (item.getItemId() == R.id.menu_print) { + printView(); + return true; } + return super.onOptionsItemSelected(item); } private void printView() { PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); - final View view = findViewById(R.id.content); - PrintJob printJob = printManager.print("Print_View", + printManager.print("Print_View", new PrintDocumentAdapter() { - private PrintedPdfDocument mPdfDocument; + private static final int RESULT_LAYOUT_FAILED = 1; + private static final int RESULT_LAYOUT_FINISHED = 2; - @Override - public void onStart() { - Log.i(LOG_TAG, "onStart"); - super.onStart(); - } + private PrintAttributes mPrintAttributes; @Override - public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, - CancellationSignal cancellationSignal, LayoutResultCallback callback, - Bundle metadata) { - Log.i(LOG_TAG, "onLayout"); - - mPdfDocument = new PrintedPdfDocument(PrintActivity.this, newAttributes); - Page page = mPdfDocument.startPage(); - view.draw(page.getCanvas()); - mPdfDocument.finishPage(page); - - PrintDocumentInfo info = new PrintDocumentInfo.Builder() - .setPageCount(1) - .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT) - .create(); - callback.onLayoutFinished(info, false); + public void onLayout(final PrintAttributes oldAttributes, + final PrintAttributes newAttributes, + final CancellationSignal cancellationSignal, + final LayoutResultCallback callback, + final Bundle metadata) { + + new AsyncTask<Void, Void, Integer>() { + @Override + protected void onPreExecute() { + // First register for cancellation requests. + cancellationSignal.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel() { + cancel(true); + } + }); + mPrintAttributes = newAttributes; + } + + @Override + protected Integer doInBackground(Void... params) { + try { + // Pretend we do some layout work. + for (int i = 0; i < PAGE_COUNT; i++) { + // Be nice and respond to cancellation. + if (isCancelled()) { + return null; + } + pretendDoingLayoutWork(); + } + return RESULT_LAYOUT_FINISHED; + } catch (Exception e) { + return RESULT_LAYOUT_FAILED; + } + } + + @Override + protected void onPostExecute(Integer result) { + // The task was not cancelled, so handle the layout result. + switch (result) { + case RESULT_LAYOUT_FINISHED: { + PrintDocumentInfo info = new PrintDocumentInfo + .Builder("print_view.pdf") + .setContentType(PrintDocumentInfo + .CONTENT_TYPE_DOCUMENT) + .setPageCount(PAGE_COUNT) + .build(); + callback.onLayoutFinished(info, false); + } break; + + case RESULT_LAYOUT_FAILED: { + callback.onLayoutFailed(null); + } break; + } + } + + @Override + protected void onCancelled(Integer result) { + // Task was cancelled, report that. + callback.onLayoutCancelled(); + } + + private void pretendDoingLayoutWork() throws Exception { + + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); } @Override - public void onWrite(final List<PageRange> pages, - final FileDescriptor destination, + public void onWrite(final PageRange[] pages, + final ParcelFileDescriptor destination, final CancellationSignal canclleationSignal, final WriteResultCallback callback) { - Log.i(LOG_TAG, "onWrite"); - final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() { + + new AsyncTask<Void, Void, Integer>() { + private static final int RESULT_WRITE_FAILED = 1; + private static final int RESULT_WRITE_FINISHED = 2; + + private final SparseIntArray mWrittenPages = new SparseIntArray(); + private final PrintedPdfDocument mPdfDocument = new PrintedPdfDocument( + PrintActivity.this, mPrintAttributes); + @Override - protected Void doInBackground(Void... params) { - try { - mPdfDocument.writeTo(new FileOutputStream(destination)); - } finally { - mPdfDocument.close(); - IoUtils.closeQuietly(destination); + protected void onPreExecute() { + // First register for cancellation requests. + canclleationSignal.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel() { + cancel(true); + } + }); + + for (int i = 0; i < PAGE_COUNT; i++) { + // Be nice and respond to cancellation. + if (isCancelled()) { + return; + } + + // Write the page only if it was requested. + if (containsPage(pages, i)) { + mWrittenPages.append(mWrittenPages.size(), i); + Page page = mPdfDocument.startPage(i); + // The page of the PDF backed canvas size is in pixels (1/72") and + // smaller that the view. We scale down the drawn content and to + // fit. This does not lead to losing data as PDF is a vector format. + final float scale = (float) Math.min(mPdfDocument.getPageWidth(), + mPdfDocument.getPageHeight()) / Math.max(view.getWidth(), view.getHeight()); + page.getCanvas().scale(scale, scale); + view.draw(page.getCanvas()); + mPdfDocument.finishPage(page); + } } - return null; } @Override - protected void onPostExecute(Void result) { - callback.onWriteFinished(pages); + protected Integer doInBackground(Void... params) { + // Write the data and return success or failure. + try { + mPdfDocument.writeTo(new FileOutputStream( + destination.getFileDescriptor())); + return RESULT_WRITE_FINISHED; + } catch (IOException ioe) { + return RESULT_WRITE_FAILED; + } } @Override - protected void onCancelled(Void result) { - callback.onWriteFailed("Cancelled"); + protected void onPostExecute(Integer result) { + // The task was not cancelled, so handle the write result. + switch (result) { + case RESULT_WRITE_FINISHED: { + PageRange[] pageRanges = computePageRanges(mWrittenPages); + callback.onWriteFinished(pageRanges); + } break; + + case RESULT_WRITE_FAILED: { + callback.onWriteFailed(null); + } break; + } + + mPdfDocument.close(); } - }; - - canclleationSignal.setOnCancelListener(new OnCancelListener() { + @Override - public void onCancel() { - task.cancel(true); + protected void onCancelled(Integer result) { + // Task was cancelled, report that. + callback.onWriteCancelled(); + mPdfDocument.close(); } - }); - - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); } - @Override - public void onFinish() { - Log.i(LOG_TAG, "onFinish"); - super.onFinish(); + private PageRange[] computePageRanges(SparseIntArray writtenPages) { + List<PageRange> pageRanges = new ArrayList<PageRange>(); + + int start = -1; + int end = -1; + final int writtenPageCount = writtenPages.size(); + for (int i = 0; i < writtenPageCount; i++) { + if (start < 0) { + start = writtenPages.valueAt(i); + } + int oldEnd = end = start; + while (i < writtenPageCount && (end - oldEnd) <= 1) { + oldEnd = end; + end = writtenPages.valueAt(i); + i++; + } + PageRange pageRange = new PageRange(start, end); + pageRanges.add(pageRange); + start = end = -1; + } + + PageRange[] pageRangesArray = new PageRange[pageRanges.size()]; + pageRanges.toArray(pageRangesArray); + return pageRangesArray; } - }, new PrintAttributes.Builder().create()); - if (printJob != null) { - /* Yay, we scheduled something to be printed!!! */ - } + private boolean containsPage(PageRange[] pageRanges, int page) { + final int pageRangeCount = pageRanges.length; + for (int i = 0; i < pageRangeCount; i++) { + if (pageRanges[i].getStart() <= page + && pageRanges[i].getEnd() >= page) { + return true; + } + } + return false; + } + }, null); } } diff --git a/PrintApp/src/foo/bar/print/PrintedPdfDocument.java b/PrintApp/src/foo/bar/print/PrintedPdfDocument.java deleted file mode 100644 index f6cdb4a..0000000 --- a/PrintApp/src/foo/bar/print/PrintedPdfDocument.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -package foo.bar.print; - -import android.content.Context; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.hardware.display.DisplayManager; -import android.print.PrintAttributes; -import android.print.PrintAttributes.Margins; -import android.print.PrintAttributes.MediaSize; -import android.print.PrintAttributes.Resolution; -import android.print.pdf.PdfDocument; -import android.print.pdf.PdfDocument.Page; -import android.print.pdf.PdfDocument.PageInfo; -import android.util.DisplayMetrics; -import android.view.Display; - -import java.io.OutputStream; - -/** - * This class is a helper for printing content to a different media - * size. The printed content will be as large as it would be on the - * screen. - * - * THIS IS A TEST CODE AND IS HIDDEN - * - * @hide - */ -public final class PrintedPdfDocument { - private static final int MILS_PER_INCH = 1000; - - /** - * Maximum bitmap size as defined in Skia's native code (see SkCanvas.cpp, - * SkDraw.cpp) - */ - private static final int MAXMIMUM_BITMAP_SIZE = 32766; - - private final PdfDocument mDocument = PdfDocument.open(); - private final Rect mPageSize = new Rect(); - private final Rect mContentSize = new Rect(); - - private final int mCanvasDensityDpi; - private final Matrix mContentToPageTransform; - - private Matrix mTempMatrix; - - /** - * Creates a new instance. - * - * @param context Context instance for accessing resources and services. - * @param attributes The {@link PrintAttributes} to user. - */ - public PrintedPdfDocument(Context context, PrintAttributes attributes) { - MediaSize mediaSize = attributes.getMediaSize(); - Resolution resolution = attributes.getResolution(); - - // TODO: What to do if horizontal and vertical DPI differ? - mCanvasDensityDpi = Math.max(attributes.getResolution().getHorizontalDpi(), attributes - .getResolution().getVerticalDpi()); - - // Figure out the scale since the content and the target DPI may differ. - DisplayMetrics metrics = new DisplayMetrics(); - DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); - dm.getDisplay(Display.DEFAULT_DISPLAY).getMetrics(metrics); - final float scaleFactor = ((float) mCanvasDensityDpi) / metrics.densityDpi; - mContentToPageTransform = new Matrix(); - mContentToPageTransform.setScale(scaleFactor, scaleFactor); - - // Compute the size of the target canvas from the attributes. - final int pageWidth = (mediaSize.getWidthMils() / MILS_PER_INCH) - * resolution.getHorizontalDpi(); - final int pageHeight = (mediaSize.getHeightMils() / MILS_PER_INCH) - * resolution.getVerticalDpi(); - mPageSize.set(0, 0, pageWidth, pageHeight); - - // Compute the content size from the attributes. - Margins margins = attributes.getMargins(); - final int marginLeft = (margins.getLeftMils() / MILS_PER_INCH) - * resolution.getHorizontalDpi(); - final int marginTop = (margins.getTopMils() / MILS_PER_INCH) * resolution.getVerticalDpi(); - final int marginRight = (margins.getRightMils() / MILS_PER_INCH) - * resolution.getHorizontalDpi(); - final int marginBottom = (margins.getBottomMils() / MILS_PER_INCH) - * resolution.getVerticalDpi(); - mContentSize.set(mPageSize.left + marginLeft, mPageSize.top + marginTop, mPageSize.right - - marginRight, mPageSize.bottom - marginBottom); - } - - /** - * Starts a new page. - * - * @return The started page. - */ - public Page startPage() { - return startPage(null); - } - - /** - * Starts a new page. - * - * @param additionalTransform Additional initial transform. - * @return The started page. - */ - public Page startPage(Matrix additionalTransform) { - Matrix transform = null; - if (additionalTransform != null) { - if (mTempMatrix == null) { - transform = mTempMatrix = new Matrix(); - } - transform.set(mContentToPageTransform); - transform.postConcat(additionalTransform); - } else { - transform = mContentToPageTransform; - } - PageInfo pageInfo = new PageInfo.Builder(mPageSize, 0, mCanvasDensityDpi).create(); - Page page = mDocument.startPage(pageInfo); - return page; - } - - /** - * Finishes a page. - * - * @param page The page to finish. - */ - public void finishPage(Page page) { - mDocument.finishPage(page); - } - - /** - * Writes the document to a stream. - * - * @param out The destination stream. - */ - public void writeTo(OutputStream out) { - mDocument.writeTo(out); - } - - /** - * Closes the document. - */ - public void close() { - mDocument.close(); - } -} diff --git a/PrintService/Android.mk b/PrintService/Android.mk index 7c5d5fa..6d4ff7f 100644 --- a/PrintService/Android.mk +++ b/PrintService/Android.mk @@ -8,6 +8,4 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_PACKAGE_NAME := PrintService -LOCAL_CERTIFICATE := platform - include $(BUILD_PACKAGE) diff --git a/PrintService/AndroidManifest.xml b/PrintService/AndroidManifest.xml index 096e7cd..6b6dd5c 100644 --- a/PrintService/AndroidManifest.xml +++ b/PrintService/AndroidManifest.xml @@ -4,8 +4,8 @@ android:versionName="1.0" > <uses-sdk - android:minSdkVersion="KeyLimePie" - android:targetSdkVersion="KeyLimePie" /> + android:minSdkVersion="19" + android:targetSdkVersion="19" /> <application android:icon="@drawable/ic_launcher" @@ -25,13 +25,24 @@ <activity android:name=".SettingsActivity" android:label="@string/settings_activity_label" - android:permission="android.permission.BIND_PRINT_SERVICE"> + > </activity> <activity android:name=".AddPrintersActivity" - android:label="@string/add_pritners_activity_label" - android:permission="android.permission.BIND_PRINT_SERVICE"> + android:label="@string/add_pritners_activity_label"> + </activity> + + <activity + android:name=".MyDialogActivity" + android:exported="false"> </activity> + + <activity + android:name=".CustomPrintOptionsActivity" + android:exported="true" + android:permission="android.permission.START_PRINT_SERVICE_CONFIG_ACTIVITY"> + </activity> + </application> </manifest> diff --git a/PrintService/res/values/arrays.xml b/PrintService/res/values/arrays.xml new file mode 100644 index 0000000..02cb0ad --- /dev/null +++ b/PrintService/res/values/arrays.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + --> + +<resources> + + <string-array name="on_print_job_queued_actions"> + <item>@string/print_now</item> + <item>@string/print_delayed</item> + <item>@string/fail_now</item> + <item>@string/fail_delayed</item> + <item>@string/block_now</item> + <item>@string/block_delayed</item> + <item>@string/block_and_delayed_unblock</item> + </string-array> + + <string-array name="on_request_cancel_print_job_actions"> + <item>@string/cancel_yes</item> + <item>@string/cancel_no</item> + </string-array> + +</resources> diff --git a/PrintService/res/values/strings.xml b/PrintService/res/values/strings.xml index 7a4b0b1..271639a 100644 --- a/PrintService/res/values/strings.xml +++ b/PrintService/res/values/strings.xml @@ -15,4 +15,19 @@ <string name="output_tray_first">First output tray</string> <string name="output_tray_second">Second output tray</string> + <string name="print_now">Print now</string> + <string name="print_delayed">Print dealyed</string> + <string name="fail_now">Fail now</string> + <string name="fail_delayed">Fail delayed</string> + <string name="block_now">Block now</string> + <string name="block_delayed">Block delayed</string> + <string name="block_and_delayed_unblock">Block and delayed unblock</string> + <string name="cancel_yes">Cancel</string> + <string name="cancel_no">Don\'t cancel</string> + + <string name="fail_reason">Don\'t feel like doing it</string> + + <string name="on_print_job_pending_activity_title">New print job pending. What to do?</string> + <string name="on_cancle_print_job_requested_activity_title">Print job cancel reqeusted. What to do?</string> + </resources> diff --git a/PrintService/res/xml/printservice.xml b/PrintService/res/xml/printservice.xml index 667bef7..690a545 100644 --- a/PrintService/res/xml/printservice.xml +++ b/PrintService/res/xml/printservice.xml @@ -1,3 +1,4 @@ <print-service xmlns:android="http://schemas.android.com/apk/res/android" android:settingsActivity="foo.bar.printservice.SettingsActivity" - android:addPrintersActivity="foo.bar.printservice.AddPrintersActivity"/> + android:addPrintersActivity="foo.bar.printservice.AddPrintersActivity" + android:advancedPrintOptionsActivity="foo.bar.printservice.CustomPrintOptionsActivity"/> diff --git a/PrintService/src/foo/bar/printservice/CustomPrintOptionsActivity.java b/PrintService/src/foo/bar/printservice/CustomPrintOptionsActivity.java new file mode 100644 index 0000000..67eda45 --- /dev/null +++ b/PrintService/src/foo/bar/printservice/CustomPrintOptionsActivity.java @@ -0,0 +1,55 @@ +package foo.bar.printservice; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.print.PageRange; +import android.print.PrintAttributes; +import android.print.PrintAttributes.MediaSize; +import android.print.PrintJobInfo; +import android.print.PrinterCapabilitiesInfo; +import android.print.PrinterInfo; +import android.printservice.PrintService; + +public class CustomPrintOptionsActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public void onResume() { + super.onResume(); + + PrintJobInfo printJobInfo = (PrintJobInfo) getIntent().getParcelableExtra( + PrintService.EXTRA_PRINT_JOB_INFO); + PrinterInfo printerInfo = (PrinterInfo) getIntent().getParcelableExtra( + "android.intent.extra.print.EXTRA_PRINTER_INFO"); + + PrinterCapabilitiesInfo capabilities = printerInfo.getCapabilities(); + + PrintAttributes attributes = new PrintAttributes.Builder() + .setColorMode(PrintAttributes.COLOR_MODE_MONOCHROME) + .setMediaSize(MediaSize.ISO_A5) + .setResolution(capabilities.getResolutions().get(0)) + .build(); + + PrintJobInfo.Builder builder = new PrintJobInfo.Builder(printJobInfo); + builder.setAttributes(attributes); + builder.setCopies(2); + builder.setAttributes(attributes); + builder.setPages(new PageRange[] {new PageRange(1, 1), new PageRange(3, 3)}); + builder.putAdvancedOption("EXTRA_FIRST_ADVANCED_OPTION", "OPALA"); + builder.putAdvancedOption("EXTRA_SECOND_ADVANCED_OPTION", 1); + + PrintJobInfo newPrintJobInfo = builder.build(); + + Intent result = new Intent(); + result.putExtra(PrintService.EXTRA_PRINT_JOB_INFO, newPrintJobInfo); + setResult(Activity.RESULT_OK, result); + + finish(); + } +} diff --git a/PrintService/src/foo/bar/printservice/MyDialogActivity.java b/PrintService/src/foo/bar/printservice/MyDialogActivity.java new file mode 100644 index 0000000..40a2f27 --- /dev/null +++ b/PrintService/src/foo/bar/printservice/MyDialogActivity.java @@ -0,0 +1,112 @@ +package foo.bar.printservice; + +import android.app.ListActivity; +import android.os.Bundle; +import android.print.PrintJobId; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; + +public class MyDialogActivity extends ListActivity { + + private static final int ITEM_INDEX_PRINT_NOW = 0; + private static final int ITEM_INDEX_PRINT_DELAYED = 1; + private static final int ITEM_INDEX_FAIL_NOW = 2; + private static final int ITEM_INDEX_FAIL_DELAYED = 3; + private static final int ITEM_INDEX_BLOCK_NOW = 4; + private static final int ITEM_INDEX_BLOCK_DELAYED = 5; + private static final int ITEM_INDEX_BLOCK_AND_DELAYED_UNBLOCK = 6; + + private static final int ITEM_INDEX_CANCEL_YES = 0; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + PrintJobId printJobId = getIntent().getParcelableExtra( + MyPrintService.INTENT_EXTRA_PRINT_JOB_ID); + final int actionType = getIntent().getIntExtra(MyPrintService.INTENT_EXTRA_ACTION_TYPE, + MyPrintService.ACTION_TYPE_ON_PRINT_JOB_PENDING); + + if (actionType == MyPrintService.ACTION_TYPE_ON_PRINT_JOB_PENDING) { + createActionTypeOnPrintJobPendingUi(printJobId); + } else { + createActionTypeOnReqeustCancelPrintJobUi(printJobId); + } + } + + private void createActionTypeOnPrintJobPendingUi(final PrintJobId printJobId) { + setTitle(getString(R.string.on_print_job_pending_activity_title)); + + setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, + getResources().getStringArray(R.array.on_print_job_queued_actions))); + getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + switch (position) { + case ITEM_INDEX_PRINT_NOW: { + MyPrintService.peekInstance().handleQueuedPrintJob(printJobId); + finish(); + } break; + + case ITEM_INDEX_PRINT_DELAYED: { + MyPrintService.peekInstance().handleQueuedPrintJobDelayed(printJobId); + finish(); + } break; + + case ITEM_INDEX_FAIL_NOW: { + MyPrintService.peekInstance().handleFailPrintJob(printJobId); + finish(); + } break; + + case ITEM_INDEX_FAIL_DELAYED: { + MyPrintService.peekInstance().handleFailPrintJobDelayed(printJobId); + finish(); + } break; + + case ITEM_INDEX_BLOCK_NOW: { + MyPrintService.peekInstance().handleBlockPrintJob(printJobId); + finish(); + } break; + + case ITEM_INDEX_BLOCK_DELAYED: { + MyPrintService.peekInstance().handleBlockPrintJobDelayed(printJobId); + finish(); + } break; + + case ITEM_INDEX_BLOCK_AND_DELAYED_UNBLOCK: { + MyPrintService.peekInstance().handleBlockAndDelayedUnblockPrintJob( + printJobId); + finish(); + } break; + + default: { + finish(); + } break; + } + } + }); + } + + private void createActionTypeOnReqeustCancelPrintJobUi(final PrintJobId printJobId) { + setTitle(getString(R.string.on_cancle_print_job_requested_activity_title)); + + setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, + getResources().getStringArray(R.array.on_request_cancel_print_job_actions))); + getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + switch (position) { + case ITEM_INDEX_CANCEL_YES: { + MyPrintService.peekInstance().handleRequestCancelPrintJob(printJobId); + finish(); + } break; + + default: { + finish(); + } break; + } + } + }); + } +} diff --git a/PrintService/src/foo/bar/printservice/MyPrintService.java b/PrintService/src/foo/bar/printservice/MyPrintService.java index a1f809c..f04d86c 100644 --- a/PrintService/src/foo/bar/printservice/MyPrintService.java +++ b/PrintService/src/foo/bar/printservice/MyPrintService.java @@ -1,3 +1,19 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package foo.bar.printservice; import android.content.Intent; @@ -6,18 +22,21 @@ import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.ParcelFileDescriptor; import android.print.PrintAttributes; import android.print.PrintAttributes.Margins; import android.print.PrintAttributes.MediaSize; import android.print.PrintAttributes.Resolution; -import android.print.PrintAttributes.Tray; +import android.print.PrintJobId; import android.print.PrintJobInfo; +import android.print.PrinterCapabilitiesInfo; import android.print.PrinterId; import android.print.PrinterInfo; import android.printservice.PrintJob; import android.printservice.PrintService; +import android.printservice.PrinterDiscoverySession; +import android.util.ArrayMap; import android.util.Log; -import android.widget.Toast; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -29,59 +48,197 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.Map; public class MyPrintService extends PrintService { - private static final String LOG_TAG = MyPrintService.class.getSimpleName(); + private static final String LOG_TAG = "MyPrintService"; + + private static final long STANDARD_DELAY_MILLIS = 10000000; + + static final String INTENT_EXTRA_ACTION_TYPE = "INTENT_EXTRA_ACTION_TYPE"; + static final String INTENT_EXTRA_PRINT_JOB_ID = "INTENT_EXTRA_PRINT_JOB_ID"; + + static final int ACTION_TYPE_ON_PRINT_JOB_PENDING = 1; + static final int ACTION_TYPE_ON_REQUEST_CANCEL_PRINT_JOB = 2; + + private static final Object sLock = new Object(); + + private static MyPrintService sInstance; private Handler mHandler; + private AsyncTask<ParcelFileDescriptor, Void, Void> mFakePrintTask; + + private FakePrinterDiscoverySession mSession; + + private final Map<PrintJobId, PrintJob> mProcessedPrintJobs = + new ArrayMap<PrintJobId, PrintJob>(); + + public static MyPrintService peekInstance() { + synchronized (sLock) { + return sInstance; + } + } + @Override protected void onConnected() { - mHandler = new MyHandler(getMainLooper()); Log.i(LOG_TAG, "#onConnected()"); + mHandler = new MyHandler(getMainLooper()); + synchronized (sLock) { + sInstance = this; + } } @Override protected void onDisconnected() { - cancellAddingFakePrinters(); Log.i(LOG_TAG, "#onDisconnected()"); + if (mSession != null) { + mSession.cancellAddingFakePrinters(); + } + synchronized (sLock) { + sInstance = null; + } } @Override - protected void onStartPrinterDiscovery() { - Log.i(LOG_TAG, "#onStartDiscoverPrinters()"); - Message message1 = mHandler.obtainMessage(MyHandler.MESSAGE_ADD_FIRST_FAKE_PRINTER); - mHandler.sendMessageDelayed(message1, 0); - - Message message2 = mHandler.obtainMessage(MyHandler.MESSAGE_ADD_SECOND_FAKE_PRINTER); - mHandler.sendMessageDelayed(message2, 10000); + protected PrinterDiscoverySession onCreatePrinterDiscoverySession() { + Log.i(LOG_TAG, "#onCreatePrinterDiscoverySession()"); + return new FakePrinterDiscoverySession(); } @Override - protected void onStopPrinterDiscovery() { - cancellAddingFakePrinters(); - Log.i(LOG_TAG, "#onStopDiscoverPrinters()"); + protected void onRequestCancelPrintJob(final PrintJob printJob) { + Log.i(LOG_TAG, "#onRequestCancelPrintJob()"); + mProcessedPrintJobs.put(printJob.getId(), printJob); + Intent intent = new Intent(this, MyDialogActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(INTENT_EXTRA_PRINT_JOB_ID, printJob.getId()); + intent.putExtra(INTENT_EXTRA_ACTION_TYPE, ACTION_TYPE_ON_REQUEST_CANCEL_PRINT_JOB); + startActivity(intent); } @Override public void onPrintJobQueued(final PrintJob printJob) { Log.i(LOG_TAG, "#onPrintJobQueued()"); - PrintJobInfo info = printJob.getInfo(); - final File file = new File(getFilesDir(), info.getLabel() + ".pdf"); - if (file.exists()) { - file.delete(); + mProcessedPrintJobs.put(printJob.getId(), printJob); + if (printJob.isQueued()) { + printJob.start(); + } + + Intent intent = new Intent(this, MyDialogActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(INTENT_EXTRA_PRINT_JOB_ID, printJob.getId()); + intent.putExtra(INTENT_EXTRA_ACTION_TYPE, ACTION_TYPE_ON_PRINT_JOB_PENDING); + startActivity(intent); + } + + void handleRequestCancelPrintJob(PrintJobId printJobId) { + PrintJob printJob = mProcessedPrintJobs.get(printJobId); + if (printJob == null) { + return; + } + mProcessedPrintJobs.remove(printJobId); + if (printJob.isQueued() || printJob.isStarted() || printJob.isBlocked()) { + mHandler.removeMessages(MyHandler.MSG_HANDLE_DO_PRINT_JOB); + mHandler.removeMessages(MyHandler.MSG_HANDLE_FAIL_PRINT_JOB); + printJob.cancel(); + } + } + + void handleFailPrintJobDelayed(PrintJobId printJobId) { + Message message = mHandler.obtainMessage( + MyHandler.MSG_HANDLE_FAIL_PRINT_JOB, printJobId); + mHandler.sendMessageDelayed(message, STANDARD_DELAY_MILLIS); + } + + void handleFailPrintJob(PrintJobId printJobId) { + PrintJob printJob = mProcessedPrintJobs.get(printJobId); + if (printJob == null) { + return; } - AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() { + mProcessedPrintJobs.remove(printJobId); + if (printJob.isQueued() || printJob.isStarted()) { + printJob.fail(getString(R.string.fail_reason)); + } + } + + void handleBlockPrintJobDelayed(PrintJobId printJobId) { + Message message = mHandler.obtainMessage( + MyHandler.MSG_HANDLE_BLOCK_PRINT_JOB, printJobId); + mHandler.sendMessageDelayed(message, STANDARD_DELAY_MILLIS); + } + + void handleBlockPrintJob(PrintJobId printJobId) { + final PrintJob printJob = mProcessedPrintJobs.get(printJobId); + if (printJob == null) { + return; + } + + if (printJob.isStarted()) { + printJob.block("Gimme some rest, dude"); + } + } + + void handleBlockAndDelayedUnblockPrintJob(PrintJobId printJobId) { + handleBlockPrintJob(printJobId); + + Message message = mHandler.obtainMessage( + MyHandler.MSG_HANDLE_UNBLOCK_PRINT_JOB, printJobId); + mHandler.sendMessageDelayed(message, STANDARD_DELAY_MILLIS); + } + + void handleUnblockPrintJob(PrintJobId printJobId) { + final PrintJob printJob = mProcessedPrintJobs.get(printJobId); + if (printJob == null) { + return; + } + + if (printJob.isBlocked()) { + printJob.start(); + } + } + + void handleQueuedPrintJobDelayed(PrintJobId printJobId) { + final PrintJob printJob = mProcessedPrintJobs.get(printJobId); + if (printJob == null) { + return; + } + + if (printJob.isQueued()) { + printJob.start(); + } + Message message = mHandler.obtainMessage( + MyHandler.MSG_HANDLE_DO_PRINT_JOB, printJobId); + mHandler.sendMessageDelayed(message, STANDARD_DELAY_MILLIS); + } + + void handleQueuedPrintJob(PrintJobId printJobId) { + final PrintJob printJob = mProcessedPrintJobs.get(printJobId); + if (printJob == null) { + return; + } + + if (printJob.isQueued()) { + printJob.start(); + } + + final PrintJobInfo info = printJob.getInfo(); + final File file = new File(getFilesDir(), info.getLabel() + ".pdf"); + + mFakePrintTask = new AsyncTask<ParcelFileDescriptor, Void, Void>() { @Override - protected Void doInBackground(Void... params) { - InputStream in = new BufferedInputStream( - new FileInputStream(printJob.getDocument().getData())); + protected Void doInBackground(ParcelFileDescriptor... params) { + InputStream in = new BufferedInputStream(new FileInputStream( + params[0].getFileDescriptor())); OutputStream out = null; try { out = new BufferedOutputStream(new FileOutputStream(file)); final byte[] buffer = new byte[8192]; while (true) { + if (isCancelled()) { + break; + } final int readByteCount = in.read(buffer); if (readByteCount < 0) { break; @@ -89,155 +246,227 @@ public class MyPrintService extends PrintService { out.write(buffer, 0, readByteCount); } } catch (IOException ioe) { - /* ignore */ + throw new RuntimeException(ioe); } finally { - try { - in.close(); - } catch (IOException ioe) { - /* ignore */ + if (in != null) { + try { + in.close(); + } catch (IOException ioe) { + /* ignore */ + } } if (out != null) { try { out.close(); } catch (IOException ioe) { - /* ignore */ + /* ignore */ } } + if (isCancelled()) { + file.delete(); + } } return null; } @Override protected void onPostExecute(Void result) { - file.setExecutable(true, false); - file.setWritable(true, false); + if (printJob.isStarted()) { + printJob.complete(); + } + file.setReadable(true, false); + // Quick and dirty to show the file - use a content provider instead. Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(Uri.fromFile(file), "application/pdf"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent, null); - if (printJob.isQueued()) { - printJob.start(); - } - - PrintJobInfo info = printJob.getInfo(); - Toast.makeText(MyPrintService.this, - "Printer: " + info.getPrinterId().getLocalId() - + " copies: " + info.getAttributes().getCopies(), - Toast.LENGTH_SHORT).show(); + mFakePrintTask = null; + } + @Override + protected void onCancelled(Void result) { if (printJob.isStarted()) { - printJob.complete(); + printJob.cancel(); } } }; - task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); - } - - private void addFirstFakePrinter() { - PrinterId printerId = generatePrinterId("1"); - PrinterInfo printer = new PrinterInfo.Builder(printerId, "Printer 1") - .setStatus(PrinterInfo.STATUS_READY) - .setMinMargins(new Margins(0, 0, 0, 0), new Margins(0, 0, 0, 0)) - .addMediaSize(MediaSize.createMediaSize(getPackageManager(), - MediaSize.ISO_A2), true) - .addMediaSize(MediaSize.createMediaSize(getPackageManager(), - MediaSize.ISO_A3), false) - .addMediaSize(MediaSize.createMediaSize(getPackageManager(), - MediaSize.ISO_A4), false) - .addMediaSize(MediaSize.createMediaSize(getPackageManager(), - MediaSize.NA_LETTER), false) - .addResolution(new Resolution("R1", getString( - R.string.resolution_600x600), 600, 600), true) - .addInputTray(new Tray("FirstInputTray", getString( - R.string.input_tray_first)), false) - .addOutputTray(new Tray("FirstOutputTray", getString( - R.string.output_tray_first)), false) - .setDuplexModes(PrintAttributes.DUPLEX_MODE_NONE - | PrintAttributes.DUPLEX_MODE_LONG_EDGE - | PrintAttributes.DUPLEX_MODE_SHORT_EDGE, - PrintAttributes.DUPLEX_MODE_NONE) - .setColorModes(PrintAttributes.COLOR_MODE_COLOR - | PrintAttributes.COLOR_MODE_MONOCHROME, - PrintAttributes.COLOR_MODE_COLOR) - .setFittingModes(PrintAttributes.FITTING_MODE_NONE - | PrintAttributes.FITTING_MODE_FIT_TO_PAGE, - PrintAttributes.FITTING_MODE_NONE) - .setOrientations(PrintAttributes.ORIENTATION_PORTRAIT - | PrintAttributes.ORIENTATION_LANDSCAPE, - PrintAttributes.ORIENTATION_PORTRAIT) - .create(); - List<PrinterInfo> printers = new ArrayList<PrinterInfo>(); - printers.add(printer); - addDiscoveredPrinters(printers); - } - - private void addSecondFakePrinter() { - PrinterId printerId = generatePrinterId("2"); - PrinterInfo printer = new PrinterInfo.Builder(printerId, "Printer 2") - .setStatus(PrinterInfo.STATUS_READY) - .setMinMargins(new Margins(0, 0, 0, 0), new Margins(0, 0, 0, 0)) - .addMediaSize(MediaSize.createMediaSize(getPackageManager(), - MediaSize.ISO_A4), true) - .addMediaSize(MediaSize.createMediaSize(getPackageManager(), - MediaSize.ISO_A5), false) - .addResolution(new Resolution("R1", getString( - R.string.resolution_200x200), 200, 200), true) - .addResolution(new Resolution("R2", getString( - R.string.resolution_300x300), 300, 300), false) - .addInputTray(new Tray("FirstInputTray", getString( - R.string.input_tray_first)), false) - .addInputTray(new Tray("SecondInputTray", getString( - R.string.input_tray_second)), true) - .addOutputTray(new Tray("FirstOutputTray", getString( - R.string.output_tray_first)), false) - .addOutputTray(new Tray("SecondOutputTray", getString( - R.string.output_tray_second)), true) - .setDuplexModes(PrintAttributes.DUPLEX_MODE_NONE - | PrintAttributes.DUPLEX_MODE_LONG_EDGE - | PrintAttributes.DUPLEX_MODE_SHORT_EDGE, - PrintAttributes.DUPLEX_MODE_SHORT_EDGE) - .setColorModes(PrintAttributes.COLOR_MODE_COLOR - | PrintAttributes.COLOR_MODE_MONOCHROME, - PrintAttributes.COLOR_MODE_MONOCHROME) - .setFittingModes(PrintAttributes.FITTING_MODE_FIT_TO_PAGE - | PrintAttributes.FITTING_MODE_NONE, - PrintAttributes.FITTING_MODE_FIT_TO_PAGE) - .setOrientations(PrintAttributes.ORIENTATION_PORTRAIT - | PrintAttributes.ORIENTATION_LANDSCAPE, - PrintAttributes.ORIENTATION_LANDSCAPE) - .create(); - List<PrinterInfo> printers = new ArrayList<PrinterInfo>(); - printers.add(printer); - addDiscoveredPrinters(printers); - } - - private void cancellAddingFakePrinters() { - mHandler.removeMessages(MyHandler.MESSAGE_ADD_FIRST_FAKE_PRINTER); - mHandler.removeMessages(MyHandler.MESSAGE_ADD_FIRST_FAKE_PRINTER); + mFakePrintTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, + printJob.getDocument().getData()); } private final class MyHandler extends Handler { - - public static final int MESSAGE_ADD_FIRST_FAKE_PRINTER = 1; - public static final int MESSAGE_ADD_SECOND_FAKE_PRINTER = 2; + public static final int MSG_HANDLE_DO_PRINT_JOB = 1; + public static final int MSG_HANDLE_FAIL_PRINT_JOB = 2; + public static final int MSG_HANDLE_BLOCK_PRINT_JOB = 3; + public static final int MSG_HANDLE_UNBLOCK_PRINT_JOB = 4; public MyHandler(Looper looper) { - super(looper, null, true); + super(looper); } @Override public void handleMessage(Message message) { switch (message.what) { - case MESSAGE_ADD_FIRST_FAKE_PRINTER: { - addFirstFakePrinter(); + case MSG_HANDLE_DO_PRINT_JOB: { + PrintJobId printJobId = (PrintJobId) message.obj; + handleQueuedPrintJob(printJobId); + } break; + + case MSG_HANDLE_FAIL_PRINT_JOB: { + PrintJobId printJobId = (PrintJobId) message.obj; + handleFailPrintJob(printJobId); } break; - case MESSAGE_ADD_SECOND_FAKE_PRINTER: { - addSecondFakePrinter(); + + case MSG_HANDLE_BLOCK_PRINT_JOB: { + PrintJobId printJobId = (PrintJobId) message.obj; + handleBlockPrintJob(printJobId); + } break; + + case MSG_HANDLE_UNBLOCK_PRINT_JOB: { + PrintJobId printJobId = (PrintJobId) message.obj; + handleUnblockPrintJob(printJobId); } break; } } } + + private final class FakePrinterDiscoverySession extends PrinterDiscoverySession { + private final Handler mSesionHandler = new SessionHandler(getMainLooper()); + + private final List<PrinterInfo> mFakePrinters = new ArrayList<PrinterInfo>(); + + public FakePrinterDiscoverySession() { + for (int i = 0; i < 10; i++) { + String name = "Printer " + i; + PrinterInfo printer = new PrinterInfo + .Builder(generatePrinterId(name), name, (i % 2 == 1) + ? PrinterInfo.STATUS_UNAVAILABLE : PrinterInfo.STATUS_IDLE) + .build(); + mFakePrinters.add(printer); + } + } + + @Override + public void onDestroy() { + Log.i(LOG_TAG, "FakePrinterDiscoverySession#onDestroy()"); + mSesionHandler.removeMessages(SessionHandler.MSG_ADD_FIRST_BATCH_FAKE_PRINTERS); + mSesionHandler.removeMessages(SessionHandler.MSG_ADD_SECOND_BATCH_FAKE_PRINTERS); + } + + @Override + public void onStartPrinterDiscovery(List<PrinterId> priorityList) { + Log.i(LOG_TAG, "FakePrinterDiscoverySession#onStartPrinterDiscovery()"); + Message message1 = mSesionHandler.obtainMessage( + SessionHandler.MSG_ADD_FIRST_BATCH_FAKE_PRINTERS, this); + mSesionHandler.sendMessageDelayed(message1, 0); + + Message message2 = mSesionHandler.obtainMessage( + SessionHandler.MSG_ADD_SECOND_BATCH_FAKE_PRINTERS, this); + mSesionHandler.sendMessageDelayed(message2, 10000); + } + + @Override + public void onStopPrinterDiscovery() { + Log.i(LOG_TAG, "FakePrinterDiscoverySession#onStopPrinterDiscovery()"); + cancellAddingFakePrinters(); + } + + @Override + public void onStartPrinterStateTracking(PrinterId printerId) { + Log.i(LOG_TAG, "FakePrinterDiscoverySession#onStartPrinterStateTracking()"); + PrinterInfo printer = findPrinterInfo(printerId); + if (printer != null) { + PrinterCapabilitiesInfo capabilities = + new PrinterCapabilitiesInfo.Builder(printerId) + .setMinMargins(new Margins(200, 200, 200, 200)) + .addMediaSize(MediaSize.ISO_A4, true) + .addMediaSize(MediaSize.ISO_A5, false) + .addResolution(new Resolution("R1", getString( + R.string.resolution_200x200), 200, 200), false) + .addResolution(new Resolution("R2", getString( + R.string.resolution_300x300), 300, 300), true) + .setColorModes(PrintAttributes.COLOR_MODE_COLOR + | PrintAttributes.COLOR_MODE_MONOCHROME, + PrintAttributes.COLOR_MODE_MONOCHROME) + .build(); + + printer = new PrinterInfo.Builder(printer) + .setCapabilities(capabilities) + .build(); + + List<PrinterInfo> printers = new ArrayList<PrinterInfo>(); + printers.add(printer); + addPrinters(printers); + } + } + + @Override + public void onValidatePrinters(List<PrinterId> printerIds) { + Log.i(LOG_TAG, "FakePrinterDiscoverySession#onValidatePrinters()"); + } + + @Override + public void onStopPrinterStateTracking(PrinterId printerId) { + Log.i(LOG_TAG, "FakePrinterDiscoverySession#onStopPrinterStateTracking()"); + } + + private void addFirstBatchFakePrinters() { + List<PrinterInfo> printers = mFakePrinters.subList(0, mFakePrinters.size() / 2); + addPrinters(printers); + } + + private void addSecondBatchFakePrinters() { + List<PrinterInfo> printers = mFakePrinters.subList(0, mFakePrinters.size() / 2 + /* mFakePrinters.size() / 2, mFakePrinters.size()*/); + final int printerCount = mFakePrinters.size(); + for (int i = printerCount - 1; i >= 0; i--) { + PrinterInfo printer = new PrinterInfo.Builder(mFakePrinters.get(i)) + .setStatus(PrinterInfo.STATUS_UNAVAILABLE).build(); + printers.add(printer); + } + addPrinters(printers); + } + + private PrinterInfo findPrinterInfo(PrinterId printerId) { + List<PrinterInfo> printers = getPrinters(); + final int printerCount = getPrinters().size(); + for (int i = 0; i < printerCount; i++) { + PrinterInfo printer = printers.get(i); + if (printer.getId().equals(printerId)) { + return printer; + } + } + return null; + } + + private void cancellAddingFakePrinters() { + mSesionHandler.removeMessages(SessionHandler.MSG_ADD_FIRST_BATCH_FAKE_PRINTERS); + mSesionHandler.removeMessages(SessionHandler.MSG_ADD_SECOND_BATCH_FAKE_PRINTERS); + } + + final class SessionHandler extends Handler { + public static final int MSG_ADD_FIRST_BATCH_FAKE_PRINTERS = 1; + public static final int MSG_ADD_SECOND_BATCH_FAKE_PRINTERS = 2; + + public SessionHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_ADD_FIRST_BATCH_FAKE_PRINTERS: { + addFirstBatchFakePrinters(); + } break; + + case MSG_ADD_SECOND_BATCH_FAKE_PRINTERS: { + addSecondBatchFakePrinters(); + } break; + } + } + } + } } diff --git a/PrintService2/src/foo/bar/printservice2/MyPrintService.java b/PrintService2/src/foo/bar/printservice2/MyPrintService.java index 8a4227a..ffc91fa 100644 --- a/PrintService2/src/foo/bar/printservice2/MyPrintService.java +++ b/PrintService2/src/foo/bar/printservice2/MyPrintService.java @@ -121,7 +121,7 @@ public class MyPrintService extends PrintService { PrintJobInfo info = printJob.getInfo(); Toast.makeText(MyPrintService.this, - "Printer: " + info.getPrinterId().getLocalId() + "Printer: " + info.getPrinterId().getName() + " copies: " + info.getAttributes().getCopies(), Toast.LENGTH_SHORT).show(); @@ -166,7 +166,7 @@ public class MyPrintService extends PrintService { .create(); List<PrinterInfo> printers = new ArrayList<PrinterInfo>(); printers.add(printer); - addDiscoveredPrinters(printers); + addPrinters(printers); } private void addSecondFakePrinter() { @@ -204,7 +204,7 @@ public class MyPrintService extends PrintService { .create(); List<PrinterInfo> printers = new ArrayList<PrinterInfo>(); printers.add(printer); - addDiscoveredPrinters(printers); + addPrinters(printers); } private final class MyHandler extends Handler { diff --git a/procstatlog/procstatlog.c b/procstatlog/procstatlog.c index c4c39e3..44bf7bd 100644 --- a/procstatlog/procstatlog.c +++ b/procstatlog/procstatlog.c @@ -274,7 +274,7 @@ static struct data *read_stats(char *names[], int name_count) { } if (pid_count >= sizeof(pids) / sizeof(pids[0])) { - fprintf(stderr, "warning: >%d processes\n", pid_count); + fprintf(stderr, "warning: >%zu processes\n", pid_count); } else { pids[pid_count++] = pid; } |