1f021758934b35e3b842c6799344531d7ea2969daChris Wren/* 2f021758934b35e3b842c6799344531d7ea2969daChris Wren * Copyright (C) 2012 The Android Open Source Project 3f021758934b35e3b842c6799344531d7ea2969daChris Wren * 4f021758934b35e3b842c6799344531d7ea2969daChris Wren * Licensed under the Apache License, Version 2.0 (the "License"); 5f021758934b35e3b842c6799344531d7ea2969daChris Wren * you may not use this file except in compliance with the License. 6f021758934b35e3b842c6799344531d7ea2969daChris Wren * You may obtain a copy of the License at 7f021758934b35e3b842c6799344531d7ea2969daChris Wren * 8f021758934b35e3b842c6799344531d7ea2969daChris Wren * http://www.apache.org/licenses/LICENSE-2.0 9f021758934b35e3b842c6799344531d7ea2969daChris Wren * 10f021758934b35e3b842c6799344531d7ea2969daChris Wren * Unless required by applicable law or agreed to in writing, software 11f021758934b35e3b842c6799344531d7ea2969daChris Wren * distributed under the License is distributed on an "AS IS" BASIS, 12f021758934b35e3b842c6799344531d7ea2969daChris Wren * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13f021758934b35e3b842c6799344531d7ea2969daChris Wren * See the License for the specific language governing permissions and 14f021758934b35e3b842c6799344531d7ea2969daChris Wren * limitations under the License. 15f021758934b35e3b842c6799344531d7ea2969daChris Wren */ 16f021758934b35e3b842c6799344531d7ea2969daChris Wren 17f021758934b35e3b842c6799344531d7ea2969daChris Wrenpackage android.support.v4.app; 18f021758934b35e3b842c6799344531d7ea2969daChris Wren 19f021758934b35e3b842c6799344531d7ea2969daChris Wrenimport android.app.Notification; 20f021758934b35e3b842c6799344531d7ea2969daChris Wrenimport android.app.PendingIntent; 21f021758934b35e3b842c6799344531d7ea2969daChris Wrenimport android.content.Context; 22f021758934b35e3b842c6799344531d7ea2969daChris Wrenimport android.graphics.Bitmap; 23b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazenimport android.os.Bundle; 242bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazenimport android.os.Parcelable; 25a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazenimport android.util.Log; 26300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazenimport android.util.SparseArray; 27f021758934b35e3b842c6799344531d7ea2969daChris Wrenimport android.widget.RemoteViews; 28b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen 29b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazenimport java.lang.reflect.Field; 30f021758934b35e3b842c6799344531d7ea2969daChris Wrenimport java.util.ArrayList; 31300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazenimport java.util.List; 32f021758934b35e3b842c6799344531d7ea2969daChris Wren 33f021758934b35e3b842c6799344531d7ea2969daChris Wrenclass NotificationCompatJellybean { 34a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen public static final String TAG = "NotificationCompat"; 35a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen 36ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen // Extras keys used for Jellybean SDK and above. 37b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen static final String EXTRA_LOCAL_ONLY = "android.support.localOnly"; 38300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen static final String EXTRA_ACTION_EXTRAS = "android.support.actionExtras"; 39ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen static final String EXTRA_REMOTE_INPUTS = "android.support.remoteInputs"; 40ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen static final String EXTRA_GROUP_KEY = "android.support.groupKey"; 41ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen static final String EXTRA_GROUP_SUMMARY = "android.support.isGroupSummary"; 42ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen static final String EXTRA_SORT_KEY = "android.support.sortKey"; 43ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen static final String EXTRA_USE_SIDE_CHANNEL = "android.support.useSideChannel"; 44300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen 452bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen // Bundle keys for storing action fields in a bundle 462bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen private static final String KEY_ICON = "icon"; 472bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen private static final String KEY_TITLE = "title"; 482bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen private static final String KEY_ACTION_INTENT = "actionIntent"; 492bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen private static final String KEY_EXTRAS = "extras"; 502bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen private static final String KEY_REMOTE_INPUTS = "remoteInputs"; 512bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen 52a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen private static final Object sExtrasLock = new Object(); 53a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen private static Field sExtrasField; 54a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen private static boolean sExtrasFieldAccessFailed; 55b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen 56300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen private static final Object sActionsLock = new Object(); 57300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen private static Class<?> sActionClass; 58300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen private static Field sActionsField; 59300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen private static Field sActionIconField; 60300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen private static Field sActionTitleField; 61300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen private static Field sActionIntentField; 62300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen private static boolean sActionsAccessFailed; 63300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen 64b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen public static class Builder implements NotificationBuilderWithBuilderAccessor, 65b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen NotificationBuilderWithActions { 66b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen private Notification.Builder b; 67ab78e9b2a147c8de7b5cf231b97aad9d8c4f106cGriff Hazen private final Bundle mExtras; 68300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen private List<Bundle> mActionExtrasList = new ArrayList<Bundle>(); 69b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen 70b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen public Builder(Context context, Notification n, 71b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, 72b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen RemoteViews tickerView, int number, 73b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon, 7443c5718722bab1f836b7c94f2ec0bc19e653037cGriff Hazen int progressMax, int progress, boolean progressIndeterminate, 75ab78e9b2a147c8de7b5cf231b97aad9d8c4f106cGriff Hazen boolean useChronometer, int priority, CharSequence subText, boolean localOnly, 76ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen Bundle extras, String groupKey, boolean groupSummary, String sortKey) { 77b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen b = new Notification.Builder(context) 78b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setWhen(n.when) 79b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setSmallIcon(n.icon, n.iconLevel) 80b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setContent(n.contentView) 81b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setTicker(n.tickerText, tickerView) 82b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setSound(n.sound, n.audioStreamType) 83b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setVibrate(n.vibrate) 84b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) 85b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) 86b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) 87b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) 88b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setDefaults(n.defaults) 89b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setContentTitle(contentTitle) 90b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setContentText(contentText) 91b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setSubText(subText) 92b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setContentInfo(contentInfo) 93b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setContentIntent(contentIntent) 94b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setDeleteIntent(n.deleteIntent) 95b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setFullScreenIntent(fullScreenIntent, 96b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) 97b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setLargeIcon(largeIcon) 98b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setNumber(number) 99b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setUsesChronometer(useChronometer) 100b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen .setPriority(priority) 10143c5718722bab1f836b7c94f2ec0bc19e653037cGriff Hazen .setProgress(progressMax, progress, progressIndeterminate); 102ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen mExtras = new Bundle(); 103ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen if (extras != null) { 104ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen mExtras.putAll(extras); 105ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } 106ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen if (localOnly) { 107ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen mExtras.putBoolean(EXTRA_LOCAL_ONLY, true); 108ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } 109ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen if (groupKey != null) { 110ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen mExtras.putString(EXTRA_GROUP_KEY, groupKey); 111ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen if (groupSummary) { 112ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen mExtras.putBoolean(EXTRA_GROUP_SUMMARY, true); 113ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } else { 114ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen mExtras.putBoolean(EXTRA_USE_SIDE_CHANNEL, true); 115ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } 116ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } 117ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen if (sortKey != null) { 118ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen mExtras.putString(EXTRA_SORT_KEY, sortKey); 119ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } 120b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen } 121b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen 122b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen @Override 123ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen public void addAction(NotificationCompatBase.Action action) { 124ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen mActionExtrasList.add(writeActionAndGetExtras(b, action)); 125b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen } 126b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen 127b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen @Override 128b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen public Notification.Builder getBuilder() { 129b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen return b; 130b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen } 131c55d0160b764bbad4e386556cab3ccac72686b0cGriff Hazen 132b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen public Notification build() { 133b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen Notification notif = b.build(); 134ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen // Merge in developer provided extras, but let the values already set 135ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen // for keys take precedence. 136ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen Bundle extras = getExtras(notif); 137ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen Bundle mergeBundle = new Bundle(mExtras); 138ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen for (String key : mExtras.keySet()) { 139ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen if (extras.containsKey(key)) { 140ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen mergeBundle.remove(key); 141ab78e9b2a147c8de7b5cf231b97aad9d8c4f106cGriff Hazen } 142300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 143ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen extras.putAll(mergeBundle); 144300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen SparseArray<Bundle> actionExtrasMap = buildActionExtrasMap(mActionExtrasList); 145300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen if (actionExtrasMap != null) { 146300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen // Add the action extras sparse array if any action was added with extras. 147300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen getExtras(notif).putSparseParcelableArray(EXTRA_ACTION_EXTRAS, actionExtrasMap); 148b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen } 149b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen return notif; 150b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen } 151f021758934b35e3b842c6799344531d7ea2969daChris Wren } 152f021758934b35e3b842c6799344531d7ea2969daChris Wren 153b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen public static void addBigTextStyle(NotificationBuilderWithBuilderAccessor b, 154b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen CharSequence bigContentTitle, boolean useSummary, 155f021758934b35e3b842c6799344531d7ea2969daChris Wren CharSequence summaryText, CharSequence bigText) { 156b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen Notification.BigTextStyle style = new Notification.BigTextStyle(b.getBuilder()) 157f021758934b35e3b842c6799344531d7ea2969daChris Wren .setBigContentTitle(bigContentTitle) 158f021758934b35e3b842c6799344531d7ea2969daChris Wren .bigText(bigText); 159f021758934b35e3b842c6799344531d7ea2969daChris Wren if (useSummary) { 160f021758934b35e3b842c6799344531d7ea2969daChris Wren style.setSummaryText(summaryText); 161fe3b5bac4901a4bb8cf51c09fe4910b02388818aGriff Hazen } 162f021758934b35e3b842c6799344531d7ea2969daChris Wren } 163f021758934b35e3b842c6799344531d7ea2969daChris Wren 164b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen public static void addBigPictureStyle(NotificationBuilderWithBuilderAccessor b, 165b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen CharSequence bigContentTitle, boolean useSummary, 16649714d665f839c4804a17eea129092f8b472926dRoman Nurik CharSequence summaryText, Bitmap bigPicture, Bitmap bigLargeIcon, 16749714d665f839c4804a17eea129092f8b472926dRoman Nurik boolean bigLargeIconSet) { 168b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen Notification.BigPictureStyle style = new Notification.BigPictureStyle(b.getBuilder()) 169fe3b5bac4901a4bb8cf51c09fe4910b02388818aGriff Hazen .setBigContentTitle(bigContentTitle) 170fe3b5bac4901a4bb8cf51c09fe4910b02388818aGriff Hazen .bigPicture(bigPicture); 171fe3b5bac4901a4bb8cf51c09fe4910b02388818aGriff Hazen if (bigLargeIconSet) { 172fe3b5bac4901a4bb8cf51c09fe4910b02388818aGriff Hazen style.bigLargeIcon(bigLargeIcon); 173fe3b5bac4901a4bb8cf51c09fe4910b02388818aGriff Hazen } 174f021758934b35e3b842c6799344531d7ea2969daChris Wren if (useSummary) { 175f021758934b35e3b842c6799344531d7ea2969daChris Wren style.setSummaryText(summaryText); 176fe3b5bac4901a4bb8cf51c09fe4910b02388818aGriff Hazen } 177f021758934b35e3b842c6799344531d7ea2969daChris Wren } 178f021758934b35e3b842c6799344531d7ea2969daChris Wren 179b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen public static void addInboxStyle(NotificationBuilderWithBuilderAccessor b, 180b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen CharSequence bigContentTitle, boolean useSummary, 181f021758934b35e3b842c6799344531d7ea2969daChris Wren CharSequence summaryText, ArrayList<CharSequence> texts) { 182b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen Notification.InboxStyle style = new Notification.InboxStyle(b.getBuilder()) 183f021758934b35e3b842c6799344531d7ea2969daChris Wren .setBigContentTitle(bigContentTitle); 184f021758934b35e3b842c6799344531d7ea2969daChris Wren if (useSummary) { 185f021758934b35e3b842c6799344531d7ea2969daChris Wren style.setSummaryText(summaryText); 186f021758934b35e3b842c6799344531d7ea2969daChris Wren } 187f021758934b35e3b842c6799344531d7ea2969daChris Wren for (CharSequence text: texts) { 188f021758934b35e3b842c6799344531d7ea2969daChris Wren style.addLine(text); 189f021758934b35e3b842c6799344531d7ea2969daChris Wren } 190f021758934b35e3b842c6799344531d7ea2969daChris Wren } 191f021758934b35e3b842c6799344531d7ea2969daChris Wren 192300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen /** Return an SparseArray for action extras or null if none was needed. */ 193300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen public static SparseArray<Bundle> buildActionExtrasMap(List<Bundle> actionExtrasList) { 194300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen SparseArray<Bundle> actionExtrasMap = null; 195300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen for (int i = 0, count = actionExtrasList.size(); i < count; i++) { 196300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen Bundle actionExtras = actionExtrasList.get(i); 197300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen if (actionExtras != null) { 198300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen if (actionExtrasMap == null) { 199300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen actionExtrasMap = new SparseArray<Bundle>(); 200300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 201300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen actionExtrasMap.put(i, actionExtras); 202300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 203300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 204300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen return actionExtrasMap; 205300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 206300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen 207b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen /** 208b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen * Get the extras Bundle from a notification using reflection. Extras were present in 209b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen * Jellybean notifications, but the field was private until KitKat. 210b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen */ 211b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen public static Bundle getExtras(Notification notif) { 212a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen synchronized (sExtrasLock) { 213a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen if (sExtrasFieldAccessFailed) { 214a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen return null; 215b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen } 216a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen try { 217a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen if (sExtrasField == null) { 218a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen Field extrasField = Notification.class.getDeclaredField("extras"); 219a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen if (!Bundle.class.isAssignableFrom(extrasField.getType())) { 220a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen Log.e(TAG, "Notification.extras field is not of type Bundle"); 221a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen sExtrasFieldAccessFailed = true; 222a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen return null; 223a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen } 224a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen extrasField.setAccessible(true); 225a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen sExtrasField = extrasField; 226a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen } 227a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen Bundle extras = (Bundle) sExtrasField.get(notif); 228a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen if (extras == null) { 229a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen extras = new Bundle(); 230a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen sExtrasField.set(notif, extras); 231a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen } 232a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen return extras; 233a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen } catch (IllegalAccessException e) { 234a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen Log.e(TAG, "Unable to access notification extras", e); 235a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen } catch (NoSuchFieldException e) { 236a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen Log.e(TAG, "Unable to access notification extras", e); 237b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen } 238a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen sExtrasFieldAccessFailed = true; 239a091d8267055595d1bb41abb53c73c61cce1484aGriff Hazen return null; 240b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen } 241b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen } 242b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen 2432bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen public static NotificationCompatBase.Action readAction( 2442bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen NotificationCompatBase.Action.Factory factory, 2452bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory, int icon, 2462bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen CharSequence title, PendingIntent actionIntent, Bundle extras) { 247ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen RemoteInputCompatBase.RemoteInput[] remoteInputs = null; 248ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen if (extras != null) { 249ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen remoteInputs = RemoteInputCompatJellybean.fromBundleArray( 250ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen BundleUtil.getBundleArrayFromBundle(extras, EXTRA_REMOTE_INPUTS), 251ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen remoteInputFactory); 252ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } 253ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen return factory.build(icon, title, actionIntent, extras, remoteInputs); 254ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } 255ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen 256ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen public static Bundle writeActionAndGetExtras( 257ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen Notification.Builder builder, NotificationCompatBase.Action action) { 258ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen builder.addAction(action.getIcon(), action.getTitle(), action.getActionIntent()); 259ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen Bundle actionExtras = new Bundle(action.getExtras()); 260ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen if (action.getRemoteInputs() != null) { 261ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen actionExtras.putParcelableArray(EXTRA_REMOTE_INPUTS, 262ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen RemoteInputCompatJellybean.toBundleArray(action.getRemoteInputs())); 263ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } 264ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen return actionExtras; 265ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } 266ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen 267300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen public static int getActionCount(Notification notif) { 268300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen synchronized (sActionsLock) { 269300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen Object[] actionObjects = getActionObjectsLocked(notif); 270300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen return actionObjects != null ? actionObjects.length : 0; 271300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 272300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 273300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen 274ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen public static NotificationCompatBase.Action getAction(Notification notif, int actionIndex, 2752bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen NotificationCompatBase.Action.Factory factory, 2762bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { 277300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen synchronized (sActionsLock) { 278300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen try { 279300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen Object actionObject = getActionObjectsLocked(notif)[actionIndex]; 280300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen Bundle actionExtras = null; 281300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen Bundle extras = getExtras(notif); 282300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen if (extras != null) { 283300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen SparseArray<Bundle> actionExtrasMap = extras.getSparseParcelableArray( 284300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen EXTRA_ACTION_EXTRAS); 285300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen if (actionExtrasMap != null) { 286300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen actionExtras = actionExtrasMap.get(actionIndex); 287300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 288300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 289ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen return readAction(factory, remoteInputFactory, 290ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen sActionIconField.getInt(actionObject), 291300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen (CharSequence) sActionTitleField.get(actionObject), 292300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen (PendingIntent) sActionIntentField.get(actionObject), 293300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen actionExtras); 294300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } catch (IllegalAccessException e) { 295300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen Log.e(TAG, "Unable to access notification actions", e); 296300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen sActionsAccessFailed = true; 297300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 298300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 299ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen return null; 300300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 301300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen 302300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen private static Object[] getActionObjectsLocked(Notification notif) { 303300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen synchronized (sActionsLock) { 304300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen if (!ensureActionReflectionReadyLocked()) { 305300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen return null; 306300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 307300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen try { 308300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen return (Object[]) sActionsField.get(notif); 309300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } catch (IllegalAccessException e) { 310300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen Log.e(TAG, "Unable to access notification actions", e); 311300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen sActionsAccessFailed = true; 312300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen return null; 313300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 314300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 315300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 316300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen 317300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen private static boolean ensureActionReflectionReadyLocked() { 318300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen if (sActionsAccessFailed) { 319300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen return false; 320300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 321300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen try { 322300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen if (sActionsField == null) { 323300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen sActionClass = Class.forName("android.app.Notification$Action"); 324300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen sActionIconField = sActionClass.getDeclaredField("icon"); 325300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen sActionTitleField = sActionClass.getDeclaredField("title"); 326300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen sActionIntentField = sActionClass.getDeclaredField("actionIntent"); 327300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen sActionsField = Notification.class.getDeclaredField("actions"); 328300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen sActionsField.setAccessible(true); 329300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 330300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } catch (ClassNotFoundException e) { 331300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen Log.e(TAG, "Unable to access notification actions", e); 332300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen sActionsAccessFailed = true; 333300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } catch (NoSuchFieldException e) { 334300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen Log.e(TAG, "Unable to access notification actions", e); 335300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen sActionsAccessFailed = true; 336300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 337300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen return !sActionsAccessFailed; 338300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen } 339300ad7c234a0ccfc41ae7fdbdcdd57faece2a8e0Griff Hazen 3402bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen public static NotificationCompatBase.Action[] getActionsFromParcelableArrayList( 3412bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen ArrayList<Parcelable> parcelables, 3422bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen NotificationCompatBase.Action.Factory actionFactory, 3432bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { 3442bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen if (parcelables == null) { 3452bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen return null; 3462bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen } 3472bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen NotificationCompatBase.Action[] actions = actionFactory.newArray(parcelables.size()); 3482bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen for (int i = 0; i < actions.length; i++) { 3492bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen actions[i] = getActionFromBundle((Bundle) parcelables.get(i), 3502bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen actionFactory, remoteInputFactory); 3512bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen } 3522bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen return actions; 3532bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen } 3542bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen 3552bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen private static NotificationCompatBase.Action getActionFromBundle(Bundle bundle, 3562bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen NotificationCompatBase.Action.Factory actionFactory, 3572bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { 3582bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen return actionFactory.build( 3592bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen bundle.getInt(KEY_ICON), 3602bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen bundle.getCharSequence(KEY_TITLE), 3612bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen bundle.<PendingIntent>getParcelable(KEY_ACTION_INTENT), 3622bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen bundle.getBundle(KEY_EXTRAS), 3632bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen RemoteInputCompatJellybean.fromBundleArray( 3642bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen BundleUtil.getBundleArrayFromBundle(bundle, KEY_REMOTE_INPUTS), 3652bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen remoteInputFactory)); 3662bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen } 3672bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen 3682bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen public static ArrayList<Parcelable> getParcelableArrayListForActions( 3692bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen NotificationCompatBase.Action[] actions) { 3702bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen if (actions == null) { 3712bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen return null; 3722bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen } 3732bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen ArrayList<Parcelable> parcelables = new ArrayList<Parcelable>(actions.length); 3742bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen for (NotificationCompatBase.Action action : actions) { 3752bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen parcelables.add(getBundleForAction(action)); 3762bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen } 3772bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen return parcelables; 3782bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen } 3792bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen 3802bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen private static Bundle getBundleForAction(NotificationCompatBase.Action action) { 3812bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen Bundle bundle = new Bundle(); 3822bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen bundle.putInt(KEY_ICON, action.getIcon()); 3832bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen bundle.putCharSequence(KEY_TITLE, action.getTitle()); 3842bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen bundle.putParcelable(KEY_ACTION_INTENT, action.getActionIntent()); 3852bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen bundle.putBundle(KEY_EXTRAS, action.getExtras()); 3862bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen bundle.putParcelableArray(KEY_REMOTE_INPUTS, RemoteInputCompatJellybean.toBundleArray( 3872bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen action.getRemoteInputs())); 3882bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen return bundle; 3892bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen } 3902bb98d48fdaf79a2bbd9d247da81a2bb9834dfc7Griff Hazen 391b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen public static boolean getLocalOnly(Notification notif) { 392b56de0d1a113c71a2808303009ab4d9708ed6e84Griff Hazen return getExtras(notif).getBoolean(EXTRA_LOCAL_ONLY); 39362d32dda7a6dd510ba7bbf11bb3edaa314c4948eGriff Hazen } 394ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen 395ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen public static String getGroup(Notification n) { 396ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen return getExtras(n).getString(EXTRA_GROUP_KEY); 397ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } 398ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen 399ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen public static boolean isGroupSummary(Notification n) { 400ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen return getExtras(n).getBoolean(EXTRA_GROUP_SUMMARY); 401ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } 402ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen 403ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen public static String getSortKey(Notification n) { 404ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen return getExtras(n).getString(EXTRA_SORT_KEY); 405ce16e4276c2f61109a23b3f6707cfcd87b07c735Griff Hazen } 406f021758934b35e3b842c6799344531d7ea2969daChris Wren} 407