From 8e7a5e3c745ee2ca8078217f6e28fabc5785b7f2 Mon Sep 17 00:00:00 2001 From: Forrest Guice Date: Tue, 12 Nov 2024 16:12:33 -0700 Subject: [PATCH 1/6] AlarmNotifications Enforce a time-out when querying addon alarms to avoid ContentProvider caused ANRs (after failure to respond under 1000ms). (#842) --- .../alarmclock/AlarmNotifications.java | 123 ++++++++++++++---- 1 file changed, 96 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmNotifications.java b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmNotifications.java index 56b534fb2..e5ae43b10 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmNotifications.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmNotifications.java @@ -92,6 +92,13 @@ import java.util.HashSet; import java.util.Set; import java.util.TimeZone; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; public class AlarmNotifications extends BroadcastReceiver { @@ -134,6 +141,8 @@ public class AlarmNotifications extends BroadcastReceiver public static final int NOTIFICATION_BEDTIME_ACTIVE_ID = -1000; + public static final int NOTIFICATION_ERROR_ID = -9999; + public static final String ACTION_LOCKED_BOOT_COMPLETED; static { if (Build.VERSION.SDK_INT >= 24) { @@ -1386,6 +1395,19 @@ public static NotificationCompat.Builder warningNotificationBuilder(Context cont return builder; } + public static Notification createWarningNotification(Context context, String message) + { + NotificationCompat.Builder builder = warningNotificationBuilder(context); + builder.setContentText(message); + + NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle(); + style.setBigContentTitle(context.getString(R.string.app_name_alarmclock)); + style.bigText(message); + builder.setStyle(style); + + return builder.build(); + } + public static Notification createAutostartWarningNotification(Context context) { NotificationCompat.Builder builder = warningNotificationBuilder(context); @@ -2611,7 +2633,7 @@ public static boolean updateAlarmTime(Context context, final AlarmClockItem item eventTime = updateAlarmTime_solarEvent(context, event, item.location, item.offset, item.repeating, repeatingDays, now); } else if (eventID != null) { - eventTime = updateAlarmTime_addonEvent(context.getContentResolver(), eventID, item.location, item.offset, item.repeating, repeatingDays, now); + eventTime = updateAlarmTime_addonEvent(context, context.getContentResolver(), eventID, item.location, item.offset, item.repeating, repeatingDays, now); } else { modifyHourMinute = false; // "clock time" alarms should leave "hour" and "minute" values untouched @@ -2866,7 +2888,7 @@ private static Calendar updateAlarmTime_seasonEvent(Context context, @NonNull So return eventTime; } - protected static Calendar updateAlarmTime_addonEvent(@Nullable ContentResolver resolver, @NonNull String eventID, @Nullable Location location, long offset, boolean repeating, @NonNull ArrayList repeatingDays, @NonNull Calendar now) + protected static Calendar updateAlarmTime_addonEvent(Context context, @Nullable ContentResolver resolver, @NonNull String eventID, @Nullable Location location, long offset, boolean repeating, @NonNull ArrayList repeatingDays, @NonNull Calendar now) { if (repeatingDays.isEmpty()) { //Log.w(TAG, "updateAlarmTime_addonEvent: empty repeatingDays! using EVERYDAY instead.."); @@ -2875,38 +2897,44 @@ protected static Calendar updateAlarmTime_addonEvent(@Nullable ContentResolver r Log.d(TAG, "updateAlarmTime_addonEvent: eventID: " + eventID + ", offset: " + offset + ", repeating: " + repeating + ", repeatingDays: " + repeatingDays); long nowMillis = now.getTimeInMillis(); - Uri uri_id = Uri.parse(eventID); Uri uri_calc = Uri.parse(AlarmAddon.getEventCalcUri(uri_id.getAuthority(), uri_id.getLastPathSegment())); - if (resolver != null) - { - StringBuilder repeatingDaysString = new StringBuilder("["); - if (repeating) { - for (int i = 0; i < repeatingDays.size(); i++) { - repeatingDaysString.append(repeatingDays.get(i)); - if (i != repeatingDays.size() - 1) { - repeatingDaysString.append(","); - } + + StringBuilder repeatingDaysString = new StringBuilder("["); + if (repeating) { + for (int i = 0; i < repeatingDays.size(); i++) { + repeatingDaysString.append(repeatingDays.get(i)); + if (i != repeatingDays.size() - 1) { + repeatingDaysString.append(","); } } - repeatingDaysString.append("]"); + } + repeatingDaysString.append("]"); - String[] selectionArgs = new String[] { Long.toString(nowMillis), Long.toString(offset), Boolean.toString(repeating), repeatingDaysString.toString() }; - String selection = AlarmEventContract.EXTRA_ALARM_NOW + "=? AND " - + AlarmEventContract.EXTRA_ALARM_OFFSET + "=? AND " - + AlarmEventContract.EXTRA_ALARM_REPEAT + "=? AND " - + AlarmEventContract.EXTRA_ALARM_REPEAT_DAYS + "=?"; + String[] selectionArgs = new String[] { Long.toString(nowMillis), Long.toString(offset), Boolean.toString(repeating), repeatingDaysString.toString() }; + String selection = AlarmEventContract.EXTRA_ALARM_NOW + "=? AND " + + AlarmEventContract.EXTRA_ALARM_OFFSET + "=? AND " + + AlarmEventContract.EXTRA_ALARM_REPEAT + "=? AND " + + AlarmEventContract.EXTRA_ALARM_REPEAT_DAYS + "=?"; - if (location != null) - { - selectionArgs = new String[] { Long.toString(nowMillis), Long.toString(offset), Boolean.toString(repeating), repeatingDaysString.toString(), - location.getLatitude(), location.getLongitude(), location.getAltitude() }; - selection += " AND " - + CalculatorProviderContract.COLUMN_CONFIG_LATITUDE + "=? AND " - + CalculatorProviderContract.COLUMN_CONFIG_LONGITUDE + "=? AND " - + CalculatorProviderContract.COLUMN_CONFIG_ALTITUDE + "=?"; - } + if (location != null) + { + selectionArgs = new String[] { Long.toString(nowMillis), Long.toString(offset), Boolean.toString(repeating), repeatingDaysString.toString(), + location.getLatitude(), location.getLongitude(), location.getAltitude() }; + selection += " AND " + + CalculatorProviderContract.COLUMN_CONFIG_LATITUDE + "=? AND " + + CalculatorProviderContract.COLUMN_CONFIG_LONGITUDE + "=? AND " + + CalculatorProviderContract.COLUMN_CONFIG_ALTITUDE + "=?"; + } + return queryAddonAlarmTimeWithTimeout(context, resolver, uri_calc, selection, selectionArgs, offset, now, MAX_WAIT_MS); + } + + protected static Calendar queryAddonAlarmTime(@Nullable ContentResolver resolver, Uri uri_calc, String selection, String[] selectionArgs, long offset, Calendar now) + { + if (resolver != null) + { + long nowMillis = now.getTimeInMillis(); Cursor cursor = resolver.query(uri_calc, AlarmEventContract.QUERY_EVENT_CALC_PROJECTION, selection, selectionArgs, null); if (cursor != null) { @@ -2944,6 +2972,47 @@ protected static Calendar updateAlarmTime_addonEvent(@Nullable ContentResolver r } } + public static final long MAX_WAIT_MS = 990; + public static Calendar queryAddonAlarmTimeWithTimeout(final Context context, final ContentResolver resolver, final Uri uri_calc, final String selection, final String[] selectionArgs, final long offset, final Calendar now, long timeoutAfter) + { + ExecutorService executor = Executors.newSingleThreadExecutor(); + final CompletableFuture future = new CompletableFuture<>(); + final Future task = executor.submit(new Runnable() + { + @Override + public void run() + { + try { + long bench_start = System.nanoTime(); + Calendar result = queryAddonAlarmTime(resolver, uri_calc, selection, selectionArgs, offset, now); + long bench_end = System.nanoTime(); + Log.d(TAG, "BENCH: querying " + uri_calc + " took " + ((bench_end - bench_start) / 1000000.0) + " ms"); + future.complete(result); + + } catch (Exception e) { + future.completeExceptionally(e); + } + } + }); + + Calendar calendar = null; + try { + calendar = (Calendar) future.get(timeoutAfter, TimeUnit.MILLISECONDS); + + } catch (TimeoutException e) { + Log.e(TAG, "updateAlarmTime: failed to query alarm time; request timed out! " + uri_calc); + Notification warningNotification = createWarningNotification(context, "Failed to schedule addon alarm! The request timed out...\n\n" + uri_calc); // TODO: i18n + showNotification(context, warningNotification, NOTIFICATION_ERROR_ID); + + } catch (InterruptedException | ExecutionException e) { + Log.e(TAG, "updateAlarmTime: failed to query alarm time; " + uri_calc + ": " + e); + + } finally { + task.cancel(true); + } + return calendar; + } + @Nullable protected static Calendar updateAlarmTime_clockTime(int hour, int minute, String tzID, @Nullable Location location, long offset, boolean repeating, @NonNull ArrayList repeatingDays, @NonNull Calendar now) { From 5d94f214239428d2632b49e82983d13c50615d02 Mon Sep 17 00:00:00 2001 From: Forrest Guice Date: Wed, 13 Nov 2024 10:29:57 -0700 Subject: [PATCH 2/6] AlarmEventItem Enforce a time-out when querying addon alarms to avoid ContentProvider caused ANRs (after failure to respond under 1000ms). (#842) --- .../suntimeswidget/alarmclock/AlarmAddon.java | 56 ++++++++++++++++++- .../suntimeswidget/alarmclock/AlarmEvent.java | 6 +- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmAddon.java b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmAddon.java index 90168d39c..54ffad4e3 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmAddon.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmAddon.java @@ -1,5 +1,5 @@ /** - Copyright (C) 2021-2022 Forrest Guice + Copyright (C) 2021-2024 Forrest Guice This file is part of SuntimesWidget. SuntimesWidget is free software: you can redistribute it and/or modify @@ -28,6 +28,7 @@ import android.content.pm.ResolveInfo; import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -40,6 +41,13 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * AlarmAddon @@ -313,6 +321,52 @@ public static boolean checkUriPermission(@NonNull Context context, @NonNull Stri return hasPermission; } + public static boolean queryDisplayStringsWithTimeout(@NonNull final AlarmEvent.AlarmEventItem item, @Nullable final ContentResolver resolver, long timeoutAfter) + { + if (item == null || resolver == null) { + Log.w("AlarmAddon", "queryDisplayStrings: item or resolver is null, returning early!"); + return false; + } + if (Build.VERSION.SDK_INT < 24) { + return queryDisplayStrings(item, resolver); + } + + ExecutorService executor = Executors.newSingleThreadExecutor(); + final CompletableFuture future = new CompletableFuture<>(); + final Future task = executor.submit(new Runnable() + { + @Override + public void run() + { + try { + long bench_start = System.nanoTime(); + boolean result = queryDisplayStrings(item, resolver); + long bench_end = System.nanoTime(); + Log.d("AlarmAddon", "BENCH: querying " + item.getUri() + " took " + ((bench_end - bench_start) / 1000000.0) + " ms"); + future.complete(result); + + } catch (Exception e) { + future.completeExceptionally(e); + } + } + }); + + Boolean result = null; + try { + result = (Boolean) future.get(timeoutAfter, TimeUnit.MILLISECONDS); + + } catch (TimeoutException e) { + Log.e("AlarmAddon", "queryDisplayStrings: failed to query AlarmEventItem display strings; request timed out! " + item.getUri()); + + } catch (InterruptedException | ExecutionException e) { + Log.e("AlarmAddon", "queryDisplayStrings: failed to query AlarmEventItem display strings; " + item.getUri() + ": " + e); + + } finally { + task.cancel(true); + } + return (result != null && result); + } + public static boolean queryDisplayStrings(@NonNull AlarmEvent.AlarmEventItem item, @Nullable ContentResolver resolver) { boolean retValue = false; diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmEvent.java b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmEvent.java index 353a7f84e..faba03ccb 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmEvent.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmEvent.java @@ -120,6 +120,8 @@ public int getQuantity() { */ public static class AlarmEventItem { + public static final long MAX_WAIT_MS = 1000; + protected SolarEvents event; protected String title = "", summary = null; protected AlarmEventPhrase phrase = null; @@ -139,7 +141,7 @@ public AlarmEventItem( @NonNull String authority, @NonNull String name, @Nullabl { event = null; uri = AlarmAddon.getEventInfoUri(authority, name); - resolved = AlarmAddon.queryDisplayStrings(this, resolver); + resolved = AlarmAddon.queryDisplayStringsWithTimeout(this, resolver, MAX_WAIT_MS); } public AlarmEventItem( @Nullable String eventUri, @Nullable ContentResolver resolver) @@ -148,7 +150,7 @@ public AlarmEventItem( @Nullable String eventUri, @Nullable ContentResolver reso if (event == null) { uri = eventUri; title = eventUri != null ? Uri.parse(eventUri).getLastPathSegment() : ""; - resolved = AlarmAddon.queryDisplayStrings(this, resolver); + resolved = AlarmAddon.queryDisplayStringsWithTimeout(this, resolver, MAX_WAIT_MS); } } From 06053658b54f0ba97f0d134bc5a61370ad2a658c Mon Sep 17 00:00:00 2001 From: Forrest Guice Date: Wed, 13 Nov 2024 14:07:44 -0700 Subject: [PATCH 3/6] WidgetListAdapter Enforce a time-out when querying addon widgets to avoid ContentProvider caused ANRs (after failure to respond under 1000ms). --- .../SuntimesWidgetListActivity.java | 2 +- .../widgets/WidgetListAdapter.java | 151 ++++++++++++++++-- 2 files changed, 137 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/SuntimesWidgetListActivity.java b/app/src/main/java/com/forrestguice/suntimeswidget/SuntimesWidgetListActivity.java index 08a5f69b8..814233078 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/SuntimesWidgetListActivity.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/SuntimesWidgetListActivity.java @@ -284,7 +284,7 @@ public void onClick(View v) { */ protected void updateViews(@NonNull Context context) { - widgetListAdapter = WidgetListAdapter.createWidgetListAdapter(context); + widgetListAdapter = WidgetListAdapter.createWidgetListAdapter(context, false); widgetList.setAdapter(widgetListAdapter); } diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/widgets/WidgetListAdapter.java b/app/src/main/java/com/forrestguice/suntimeswidget/widgets/WidgetListAdapter.java index 9e8d4b109..cd888d9ae 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/widgets/WidgetListAdapter.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/widgets/WidgetListAdapter.java @@ -34,6 +34,8 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Handler; +import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; @@ -69,9 +71,18 @@ import com.forrestguice.suntimeswidget.calculator.SuntimesMoonData; import com.forrestguice.suntimeswidget.calculator.SuntimesRiseSetData; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * A ListAdapter of WidgetListItems. @@ -103,16 +114,90 @@ public ComponentName[] getAllWidgetClasses() return components.toArray(new ComponentName[0]); } - private Context context; + private final WeakReference contextRef; private ArrayList widgets; + public WidgetListAdapter(Context context) + { + super(context, R.layout.layout_listitem_widgets); + this.contextRef = new WeakReference<>(context); + this.widgets = new ArrayList(); + } + public WidgetListAdapter(Context context, ArrayList widgets) { super(context, R.layout.layout_listitem_widgets, widgets); - this.context = context; + this.contextRef = new WeakReference<>(context); this.widgets = widgets; } + public void loadItems(Context context, Class[] widgetClasses) + { + AppWidgetManager widgetManager = AppWidgetManager.getInstance(context); + String packageName = context.getPackageName(); + ArrayList items = new ArrayList<>(); + for (Class widgetClass : widgetClasses) { + items.addAll(createWidgetListItems(context, widgetManager, packageName, widgetClass.getName())); + } + addAll(items); + } + + public void loadItems(final Context context, final List widgetInfoProviders, boolean blocking) + { + if (blocking) + { + ExecutorService executor = Executors.newSingleThreadExecutor(); + for (String uri : widgetInfoProviders) { + addAll(createWidgetListItemsWithTimeout(context, uri, executor, MAX_WAIT_MS)); + } + executor.shutdownNow(); + + } else { + final Handler handler = new Handler(Looper.getMainLooper()); + initExecutorService().submit(new Runnable() + { + public void run() + { + for (String contentUri : widgetInfoProviders) + { + final ArrayList result = createWidgetListItems(context, contentUri); + handler.post(new Runnable() { + public void run() { + addAll(result); + cleanupExecutorService(); + } + }); + } + } + }); + } + } + + private ExecutorService executor; + protected ExecutorService initExecutorService() + { + if (executor == null || executor.isShutdown()) { + executor = Executors.newSingleThreadExecutor(); + } + return executor; + } + protected void cleanupExecutorService() + { + if (executor != null) { + if (!executor.isShutdown()) { + executor.shutdownNow(); + } + executor = null; + } + } + + @Override + public void addAll(@NonNull Collection collection) + { + widgets.addAll(collection); + super.addAll(collection); + } + @Override @NonNull public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) @@ -131,7 +216,7 @@ private View widgetItemView(int position, @Nullable View convertView, @NonNull V View view = convertView; if (convertView == null) { - LayoutInflater inflater = LayoutInflater.from(context); + LayoutInflater inflater = LayoutInflater.from(contextRef.get()); view = inflater.inflate(R.layout.layout_listitem_widgets, parent, false); } @@ -204,6 +289,45 @@ public static ArrayList createWidgetListItems(Context context, @ return items; } + public static final long MAX_WAIT_MS = 1000; + + public static ArrayList createWidgetListItemsWithTimeout(@NonNull final Context context, @NonNull final String contentUri, ExecutorService executor, long timeoutAfter) + { + final CompletableFuture> future = new CompletableFuture<>(); + final Future task = executor.submit(new Runnable() + { + @Override + public void run() + { + try { + long bench_start = System.nanoTime(); + ArrayList result = createWidgetListItems(context, contentUri); + long bench_end = System.nanoTime(); + Log.d("WidgetListAdapter", "BENCH: querying " + contentUri + " took " + ((bench_end - bench_start) / 1000000.0) + " ms"); + future.complete(result); + + } catch (Exception e) { + future.completeExceptionally(e); + } + } + }); + + ArrayList result = null; + try { + result = future.get(timeoutAfter, TimeUnit.MILLISECONDS); + + } catch (TimeoutException e) { + Log.e("WidgetListAdapter", "queryDisplayStrings: failed to query AlarmEventItem display strings; request timed out! " + contentUri); + + } catch (InterruptedException | ExecutionException e) { + Log.e("WidgetListAdapter", "queryDisplayStrings: failed to query AlarmEventItem display strings; " + contentUri + ": " + e); + + } finally { + task.cancel(true); + } + return (result != null ? result : new ArrayList()); + } + public static ArrayList createWidgetListItems(@NonNull Context context, @NonNull String contentUri) { if (!contentUri.endsWith("/")) { @@ -258,18 +382,15 @@ public static ArrayList createWidgetListItems(@NonNull Context c return items; } - public static WidgetListAdapter createWidgetListAdapter(@NonNull Context context) + public static WidgetListAdapter createWidgetListAdapter(@NonNull Context context) { + return createWidgetListAdapter(context, true); + } + public static WidgetListAdapter createWidgetListAdapter(@NonNull Context context, boolean blocking) { - AppWidgetManager widgetManager = AppWidgetManager.getInstance(context); - ArrayList items = new ArrayList(); - String packageName = context.getPackageName(); - for (Class widgetClass : ALL_WIDGETS) { - items.addAll(createWidgetListItems(context, widgetManager, packageName, widgetClass.getName())); - } - for (String uri : queryWidgetInfoProviders(context)) { - items.addAll(createWidgetListItems(context, uri)); - } - return new WidgetListAdapter(context, items); + WidgetListAdapter adapter = new WidgetListAdapter(context); + adapter.loadItems(context, ALL_WIDGETS); + adapter.loadItems(context, queryWidgetInfoProviders(context), blocking); + return adapter; } private static String getTitlePattern(Context context, @NonNull String widgetClass) @@ -434,7 +555,7 @@ private static boolean hasPermission(@NonNull PackageInfo packageInfo, @NonNull /////////////////////////////////////////////////////////////////////////////////////////////// /** - * ListItem representing a running widget; specifies appWidgetId, and configuration activity.f + * ListItem representing a running widget; specifies appWidgetId, and configuration activity. */ public static class WidgetListItem { From 02faef6f0e2bda8b0a46a50465e0588857ea9799 Mon Sep 17 00:00:00 2001 From: Forrest Guice Date: Wed, 13 Nov 2024 21:13:48 -0700 Subject: [PATCH 4/6] ExecutorUtils --- .../suntimeswidget/alarmclock/AlarmAddon.java | 46 ----------- .../suntimeswidget/alarmclock/AlarmEvent.java | 18 ++++- .../alarmclock/AlarmNotifications.java | 60 +++------------ .../suntimeswidget/events/EventSettings.java | 10 ++- .../suntimeswidget/views/ExecutorUtils.java | 77 +++++++++++++++++++ 5 files changed, 110 insertions(+), 101 deletions(-) create mode 100644 app/src/main/java/com/forrestguice/suntimeswidget/views/ExecutorUtils.java diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmAddon.java b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmAddon.java index 54ffad4e3..fd8579e67 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmAddon.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmAddon.java @@ -321,52 +321,6 @@ public static boolean checkUriPermission(@NonNull Context context, @NonNull Stri return hasPermission; } - public static boolean queryDisplayStringsWithTimeout(@NonNull final AlarmEvent.AlarmEventItem item, @Nullable final ContentResolver resolver, long timeoutAfter) - { - if (item == null || resolver == null) { - Log.w("AlarmAddon", "queryDisplayStrings: item or resolver is null, returning early!"); - return false; - } - if (Build.VERSION.SDK_INT < 24) { - return queryDisplayStrings(item, resolver); - } - - ExecutorService executor = Executors.newSingleThreadExecutor(); - final CompletableFuture future = new CompletableFuture<>(); - final Future task = executor.submit(new Runnable() - { - @Override - public void run() - { - try { - long bench_start = System.nanoTime(); - boolean result = queryDisplayStrings(item, resolver); - long bench_end = System.nanoTime(); - Log.d("AlarmAddon", "BENCH: querying " + item.getUri() + " took " + ((bench_end - bench_start) / 1000000.0) + " ms"); - future.complete(result); - - } catch (Exception e) { - future.completeExceptionally(e); - } - } - }); - - Boolean result = null; - try { - result = (Boolean) future.get(timeoutAfter, TimeUnit.MILLISECONDS); - - } catch (TimeoutException e) { - Log.e("AlarmAddon", "queryDisplayStrings: failed to query AlarmEventItem display strings; request timed out! " + item.getUri()); - - } catch (InterruptedException | ExecutionException e) { - Log.e("AlarmAddon", "queryDisplayStrings: failed to query AlarmEventItem display strings; " + item.getUri() + ": " + e); - - } finally { - task.cancel(true); - } - return (result != null && result); - } - public static boolean queryDisplayStrings(@NonNull AlarmEvent.AlarmEventItem item, @Nullable ContentResolver resolver) { boolean retValue = false; diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmEvent.java b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmEvent.java index faba03ccb..a6e1d8fb3 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmEvent.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmEvent.java @@ -38,6 +38,7 @@ import com.forrestguice.suntimeswidget.events.EventIcons; import com.forrestguice.suntimeswidget.events.EventSettings; import com.forrestguice.suntimeswidget.settings.SolarEvents; +import com.forrestguice.suntimeswidget.views.ExecutorUtils; import java.util.ArrayList; import java.util.Set; @@ -137,23 +138,32 @@ public AlarmEventItem( @NonNull SolarEvents event ) { resolved = true; } - public AlarmEventItem( @NonNull String authority, @NonNull String name, @Nullable ContentResolver resolver) + public AlarmEventItem( @NonNull String authority, @NonNull String name, @Nullable final ContentResolver resolver) { event = null; uri = AlarmAddon.getEventInfoUri(authority, name); - resolved = AlarmAddon.queryDisplayStringsWithTimeout(this, resolver, MAX_WAIT_MS); + resolved = ExecutorUtils.runTask("AlarmEventItem", resolveItemTask(resolver), MAX_WAIT_MS); } - public AlarmEventItem( @Nullable String eventUri, @Nullable ContentResolver resolver) + public AlarmEventItem( @Nullable String eventUri, @Nullable final ContentResolver resolver) { event = SolarEvents.valueOf(eventUri, null); if (event == null) { uri = eventUri; title = eventUri != null ? Uri.parse(eventUri).getLastPathSegment() : ""; - resolved = AlarmAddon.queryDisplayStringsWithTimeout(this, resolver, MAX_WAIT_MS); + resolved = ExecutorUtils.runTask("AlarmEventItem", resolveItemTask(resolver), MAX_WAIT_MS); } } + private ExecutorUtils.ResultTask resolveItemTask(@Nullable final ContentResolver resolver) + { + return new ExecutorUtils.ResultTask() { + public Boolean getResult() { + return AlarmAddon.queryDisplayStrings(AlarmEventItem.this, resolver); + } + }; + } + @NonNull public String getTitle() { return (event != null ? event.getLongDisplayString() : title); diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmNotifications.java b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmNotifications.java index e5ae43b10..bc80a486d 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmNotifications.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmNotifications.java @@ -64,6 +64,7 @@ import com.forrestguice.suntimeswidget.BuildConfig; import com.forrestguice.suntimeswidget.alarmclock.bedtime.BedtimeActivity; import com.forrestguice.suntimeswidget.alarmclock.bedtime.BedtimeSettings; +import com.forrestguice.suntimeswidget.views.ExecutorUtils; import com.forrestguice.suntimeswidget.views.Toast; import com.forrestguice.suntimeswidget.R; @@ -92,13 +93,6 @@ import java.util.HashSet; import java.util.Set; import java.util.TimeZone; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; public class AlarmNotifications extends BroadcastReceiver { @@ -2926,8 +2920,17 @@ protected static Calendar updateAlarmTime_addonEvent(Context context, @Nullable + CalculatorProviderContract.COLUMN_CONFIG_LONGITUDE + "=? AND " + CalculatorProviderContract.COLUMN_CONFIG_ALTITUDE + "=?"; } + return queryAddonAlarmTimeWithTimeout(resolver, uri_calc, selection, selectionArgs, offset, now, MAX_WAIT_MS); + } - return queryAddonAlarmTimeWithTimeout(context, resolver, uri_calc, selection, selectionArgs, offset, now, MAX_WAIT_MS); + public static final long MAX_WAIT_MS = 990; + protected static Calendar queryAddonAlarmTimeWithTimeout(@Nullable final ContentResolver resolver, final Uri uri_calc, final String selection, final String[] selectionArgs, final long offset, final Calendar now, long timeoutAfter) + { + return ExecutorUtils.getResult(TAG, new ExecutorUtils.ResultTask() { + public Calendar getResult() { + return queryAddonAlarmTime(resolver, uri_calc, selection, selectionArgs, offset, now); + } + }, timeoutAfter); } protected static Calendar queryAddonAlarmTime(@Nullable ContentResolver resolver, Uri uri_calc, String selection, String[] selectionArgs, long offset, Calendar now) @@ -2972,47 +2975,6 @@ protected static Calendar queryAddonAlarmTime(@Nullable ContentResolver resolver } } - public static final long MAX_WAIT_MS = 990; - public static Calendar queryAddonAlarmTimeWithTimeout(final Context context, final ContentResolver resolver, final Uri uri_calc, final String selection, final String[] selectionArgs, final long offset, final Calendar now, long timeoutAfter) - { - ExecutorService executor = Executors.newSingleThreadExecutor(); - final CompletableFuture future = new CompletableFuture<>(); - final Future task = executor.submit(new Runnable() - { - @Override - public void run() - { - try { - long bench_start = System.nanoTime(); - Calendar result = queryAddonAlarmTime(resolver, uri_calc, selection, selectionArgs, offset, now); - long bench_end = System.nanoTime(); - Log.d(TAG, "BENCH: querying " + uri_calc + " took " + ((bench_end - bench_start) / 1000000.0) + " ms"); - future.complete(result); - - } catch (Exception e) { - future.completeExceptionally(e); - } - } - }); - - Calendar calendar = null; - try { - calendar = (Calendar) future.get(timeoutAfter, TimeUnit.MILLISECONDS); - - } catch (TimeoutException e) { - Log.e(TAG, "updateAlarmTime: failed to query alarm time; request timed out! " + uri_calc); - Notification warningNotification = createWarningNotification(context, "Failed to schedule addon alarm! The request timed out...\n\n" + uri_calc); // TODO: i18n - showNotification(context, warningNotification, NOTIFICATION_ERROR_ID); - - } catch (InterruptedException | ExecutionException e) { - Log.e(TAG, "updateAlarmTime: failed to query alarm time; " + uri_calc + ": " + e); - - } finally { - task.cancel(true); - } - return calendar; - } - @Nullable protected static Calendar updateAlarmTime_clockTime(int hour, int minute, String tzID, @Nullable Location location, long offset, boolean repeating, @NonNull ArrayList repeatingDays, @NonNull Calendar now) { diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/events/EventSettings.java b/app/src/main/java/com/forrestguice/suntimeswidget/events/EventSettings.java index 5e6572937..d03618285 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/events/EventSettings.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/events/EventSettings.java @@ -34,6 +34,7 @@ import com.forrestguice.suntimeswidget.alarmclock.AlarmEventContract; import com.forrestguice.suntimeswidget.alarmclock.AlarmEventProvider; import com.forrestguice.suntimeswidget.settings.WidgetSettings; +import com.forrestguice.suntimeswidget.views.ExecutorUtils; import java.util.ArrayList; import java.util.Arrays; @@ -157,9 +158,14 @@ public Integer getColor() { } private String summary; - public String getSummary(Context context) { + public String getSummary(final Context context) { if (summary == null) { - summary = resolveSummary(context); + summary = ExecutorUtils.getResult("getSummary", new ExecutorUtils.ResultTask() + { + public String getResult() { + return resolveSummary(context); + } + }, 1000); } return summary; } diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/views/ExecutorUtils.java b/app/src/main/java/com/forrestguice/suntimeswidget/views/ExecutorUtils.java new file mode 100644 index 000000000..e130f5f03 --- /dev/null +++ b/app/src/main/java/com/forrestguice/suntimeswidget/views/ExecutorUtils.java @@ -0,0 +1,77 @@ +/** + Copyright (C) 2024 Forrest Guice + This file is part of SuntimesWidget. + + SuntimesWidget is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SuntimesWidget is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SuntimesWidget. If not, see . +*/ + +package com.forrestguice.suntimeswidget.views; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class ExecutorUtils +{ + public interface ResultTask + { + @Nullable + T getResult(); + } + + @Nullable + public static T getResult(String tag, @NonNull final ResultTask r, long timeoutAfter) + { + ExecutorService executor = Executors.newSingleThreadExecutor(); + final CompletableFuture future = new CompletableFuture<>(); + final Future task = executor.submit(new Runnable() + { + @Override + public void run() { + try { + future.complete(r.getResult()); + } catch (Exception e) { + future.completeExceptionally(e); + } + } + }); + + T result = null; + try { + result = future.get(timeoutAfter, TimeUnit.MILLISECONDS); + + } catch (TimeoutException | InterruptedException | ExecutionException e) { + Log.e(tag, "getResult: failed! " + e); + + } finally { + task.cancel(true); + executor.shutdownNow(); + } + return result; + } + + public static boolean runTask(String tag, @NonNull final ResultTask r, long timeoutAfter) + { + Boolean result = getResult(tag, r, timeoutAfter); + return (result != null && result); + } +} From a0eb8d52685f8dad104472cf64d7cc2fb13072a7 Mon Sep 17 00:00:00 2001 From: Forrest Guice Date: Wed, 13 Nov 2024 22:34:59 -0700 Subject: [PATCH 5/6] getDefaultRingtoneUri enforce a time-out when resolving ringtone info (avoid potential ANR) --- .../suntimeswidget/alarmclock/AlarmSettings.java | 12 ++++++++++-- .../alarmclock/ui/AlarmEditActivity.java | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmSettings.java b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmSettings.java index 950eec6da..453452ccf 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmSettings.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmSettings.java @@ -57,6 +57,7 @@ import com.forrestguice.suntimeswidget.settings.AppSettings; import com.forrestguice.suntimeswidget.settings.PrefTypeInfo; import com.forrestguice.suntimeswidget.settings.WidgetActions; +import com.forrestguice.suntimeswidget.views.ExecutorUtils; import com.forrestguice.suntimeswidget.views.Toast; import java.lang.ref.WeakReference; @@ -535,15 +536,22 @@ public static Uri getFallbackRingtoneUri(Context context, AlarmClockItem.AlarmTy + (type == AlarmClockItem.AlarmType.ALARM ? R.raw.alarmsound : R.raw.notifysound)); } + public static final long MAX_WAIT_MS = 990; public static Uri getDefaultRingtoneUri(Context context, AlarmClockItem.AlarmType type) { return getDefaultRingtoneUri(context, type, false); } - public static Uri getDefaultRingtoneUri(Context context, AlarmClockItem.AlarmType type, boolean resolveDefaults) + public static Uri getDefaultRingtoneUri(final Context context, final AlarmClockItem.AlarmType type, boolean resolveDefaults) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); String uriString = prefs.getString((type == AlarmClockItem.AlarmType.ALARM) ? PREF_KEY_ALARM_RINGTONE_URI_ALARM : PREF_KEY_ALARM_RINGTONE_URI_NOTIFICATION, VALUE_RINGTONE_DEFAULT); if (resolveDefaults && VALUE_RINGTONE_DEFAULT.equals(uriString)) { - return new AlarmSettings().setDefaultRingtone(context, type); + return ExecutorUtils.getResult("defaultRingtoneUri", new ExecutorUtils.ResultTask() + { + public Uri getResult() { + Uri result = new AlarmSettings().setDefaultRingtone(context, type); + return (result != null ? result : Uri.parse(VALUE_RINGTONE_DEFAULT)); + } + }, MAX_WAIT_MS); } else return (uriString != null ? Uri.parse(uriString) : Uri.parse(VALUE_RINGTONE_DEFAULT)); } public static String getDefaultRingtoneName(Context context, AlarmClockItem.AlarmType type) diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/ui/AlarmEditActivity.java b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/ui/AlarmEditActivity.java index c02899aa2..0d9c774a7 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/ui/AlarmEditActivity.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/ui/AlarmEditActivity.java @@ -818,7 +818,7 @@ protected void ringtonePicker(@NonNull AlarmClockItem item) intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, item.type.getDisplayString()); intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, AlarmSettings.getDefaultRingtoneUri(this, item.type, true)); // TODO: setDefaultRingtoneUri may block (potential ANR)... + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, AlarmSettings.getDefaultRingtoneUri(this, item.type, true)); intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, (item.ringtoneURI != null ? Uri.parse(item.ringtoneURI) : null)); startActivityForResult(Intent.createChooser(intent, getString(R.string.configAction_setAlarmSound)), REQUEST_RINGTONE); } From f3590765153a679a2e96f1a2bc7e8efe86b50c53 Mon Sep 17 00:00:00 2001 From: Forrest Guice Date: Thu, 14 Nov 2024 17:24:35 -0700 Subject: [PATCH 6/6] ExecutorUtils --- .../suntimeswidget/alarmclock/AlarmEvent.java | 7 ++- .../alarmclock/AlarmNotifications.java | 5 +- .../alarmclock/AlarmSettings.java | 5 +- .../suntimeswidget/events/EventSettings.java | 5 +- .../suntimeswidget/views/ExecutorUtils.java | 57 +++++++++++------- .../widgets/WidgetListAdapter.java | 59 ++++++------------- 6 files changed, 68 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmEvent.java b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmEvent.java index a6e1d8fb3..402166a17 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmEvent.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmEvent.java @@ -42,6 +42,7 @@ import java.util.ArrayList; import java.util.Set; +import java.util.concurrent.Callable; import static com.forrestguice.suntimeswidget.alarmclock.AlarmEventContract.REPEAT_SUPPORT_BASIC; import static com.forrestguice.suntimeswidget.alarmclock.AlarmEventContract.REPEAT_SUPPORT_DAILY; @@ -155,10 +156,10 @@ public AlarmEventItem( @Nullable String eventUri, @Nullable final ContentResolve } } - private ExecutorUtils.ResultTask resolveItemTask(@Nullable final ContentResolver resolver) + private Callable resolveItemTask(@Nullable final ContentResolver resolver) { - return new ExecutorUtils.ResultTask() { - public Boolean getResult() { + return new Callable() { + public Boolean call() { return AlarmAddon.queryDisplayStrings(AlarmEventItem.this, resolver); } }; diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmNotifications.java b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmNotifications.java index bc80a486d..185142430 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmNotifications.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmNotifications.java @@ -93,6 +93,7 @@ import java.util.HashSet; import java.util.Set; import java.util.TimeZone; +import java.util.concurrent.Callable; public class AlarmNotifications extends BroadcastReceiver { @@ -2926,8 +2927,8 @@ protected static Calendar updateAlarmTime_addonEvent(Context context, @Nullable public static final long MAX_WAIT_MS = 990; protected static Calendar queryAddonAlarmTimeWithTimeout(@Nullable final ContentResolver resolver, final Uri uri_calc, final String selection, final String[] selectionArgs, final long offset, final Calendar now, long timeoutAfter) { - return ExecutorUtils.getResult(TAG, new ExecutorUtils.ResultTask() { - public Calendar getResult() { + return ExecutorUtils.getResult(TAG, new Callable() { + public Calendar call() { return queryAddonAlarmTime(resolver, uri_calc, selection, selectionArgs, offset, now); } }, timeoutAfter); diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmSettings.java b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmSettings.java index 453452ccf..61ada844b 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmSettings.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/alarmclock/AlarmSettings.java @@ -64,6 +64,7 @@ import java.util.Map; import java.util.TimeZone; import java.util.TreeMap; +import java.util.concurrent.Callable; import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE; @@ -545,9 +546,9 @@ public static Uri getDefaultRingtoneUri(final Context context, final AlarmClockI SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); String uriString = prefs.getString((type == AlarmClockItem.AlarmType.ALARM) ? PREF_KEY_ALARM_RINGTONE_URI_ALARM : PREF_KEY_ALARM_RINGTONE_URI_NOTIFICATION, VALUE_RINGTONE_DEFAULT); if (resolveDefaults && VALUE_RINGTONE_DEFAULT.equals(uriString)) { - return ExecutorUtils.getResult("defaultRingtoneUri", new ExecutorUtils.ResultTask() + return ExecutorUtils.getResult("defaultRingtoneUri", new Callable() { - public Uri getResult() { + public Uri call() { Uri result = new AlarmSettings().setDefaultRingtone(context, type); return (result != null ? result : Uri.parse(VALUE_RINGTONE_DEFAULT)); } diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/events/EventSettings.java b/app/src/main/java/com/forrestguice/suntimeswidget/events/EventSettings.java index d03618285..1906fdf54 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/events/EventSettings.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/events/EventSettings.java @@ -41,6 +41,7 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.Callable; import static com.forrestguice.suntimeswidget.alarmclock.AlarmEventContract.AUTHORITY; @@ -160,9 +161,9 @@ public Integer getColor() { private String summary; public String getSummary(final Context context) { if (summary == null) { - summary = ExecutorUtils.getResult("getSummary", new ExecutorUtils.ResultTask() + summary = ExecutorUtils.getResult("getSummary", new Callable() { - public String getResult() { + public String call() { return resolveSummary(context); } }, 1000); diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/views/ExecutorUtils.java b/app/src/main/java/com/forrestguice/suntimeswidget/views/ExecutorUtils.java index e130f5f03..2528029a2 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/views/ExecutorUtils.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/views/ExecutorUtils.java @@ -22,7 +22,7 @@ import android.support.annotation.Nullable; import android.util.Log; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -32,46 +32,63 @@ public class ExecutorUtils { - public interface ResultTask + public static boolean runTask(String tag, @NonNull final Callable r, long timeoutAfter) { - @Nullable - T getResult(); + Boolean result = getResult(tag, r, timeoutAfter); + return (result != null && result); + } + + @Nullable + public static T getResult(String tag, @NonNull final Callable callable, long timeoutAfter) + { + ExecutorService executor = Executors.newSingleThreadExecutor(); + final Future task = executor.submit(callable); + try { + return task.get(timeoutAfter, TimeUnit.MILLISECONDS); + + } catch (TimeoutException | InterruptedException | ExecutionException e) { + Log.e(tag, "getResult: failed! " + e); + return null; + + } finally { + task.cancel(true); + executor.shutdownNow(); + } } + // same as above, except using CompletableFuture + /*@TargetApi(24) @Nullable - public static T getResult(String tag, @NonNull final ResultTask r, long timeoutAfter) + public static T getResult(String tag, @NonNull final Callable callable, long timeoutAfter) { ExecutorService executor = Executors.newSingleThreadExecutor(); final CompletableFuture future = new CompletableFuture<>(); final Future task = executor.submit(new Runnable() { @Override - public void run() { - try { - future.complete(r.getResult()); - } catch (Exception e) { - future.completeExceptionally(e); + public void run() + { + if (Build.VERSION.SDK_INT >= 24) + { + try { + future.complete(callable.call()); + } catch (Exception e) { + future.completeExceptionally(e); + } } } }); - T result = null; try { - result = future.get(timeoutAfter, TimeUnit.MILLISECONDS); + return future.get(timeoutAfter, TimeUnit.MILLISECONDS); } catch (TimeoutException | InterruptedException | ExecutionException e) { Log.e(tag, "getResult: failed! " + e); + return null; } finally { task.cancel(true); executor.shutdownNow(); } - return result; - } - - public static boolean runTask(String tag, @NonNull final ResultTask r, long timeoutAfter) - { - Boolean result = getResult(tag, r, timeoutAfter); - return (result != null && result); - } + }*/ } diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/widgets/WidgetListAdapter.java b/app/src/main/java/com/forrestguice/suntimeswidget/widgets/WidgetListAdapter.java index cd888d9ae..96e57a3f4 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/widgets/WidgetListAdapter.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/widgets/WidgetListAdapter.java @@ -70,12 +70,14 @@ import com.forrestguice.suntimeswidget.calculator.SuntimesEquinoxSolsticeData; import com.forrestguice.suntimeswidget.calculator.SuntimesMoonData; import com.forrestguice.suntimeswidget.calculator.SuntimesRiseSetData; +import com.forrestguice.suntimeswidget.views.ExecutorUtils; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -147,8 +149,20 @@ public void loadItems(final Context context, final List widgetInfoProvid if (blocking) { ExecutorService executor = Executors.newSingleThreadExecutor(); - for (String uri : widgetInfoProviders) { - addAll(createWidgetListItemsWithTimeout(context, uri, executor, MAX_WAIT_MS)); + for (final String uri : widgetInfoProviders) + { + ArrayList items = ExecutorUtils.getResult("WidgetListAdapter", new Callable>() + { + @Override + public ArrayList call() + { + long bench_start = System.nanoTime(); + ArrayList result = createWidgetListItems(context, uri); + Log.d("WidgetListAdapter", "BENCH: querying " + uri + " took " + ((System.nanoTime() - bench_start) / 1000000.0) + " ms"); + return result; + } + }, MAX_WAIT_MS); + addAll(items); } executor.shutdownNow(); @@ -173,6 +187,8 @@ public void run() { } } + public static final long MAX_WAIT_MS = 1000; + private ExecutorService executor; protected ExecutorService initExecutorService() { @@ -289,45 +305,6 @@ public static ArrayList createWidgetListItems(Context context, @ return items; } - public static final long MAX_WAIT_MS = 1000; - - public static ArrayList createWidgetListItemsWithTimeout(@NonNull final Context context, @NonNull final String contentUri, ExecutorService executor, long timeoutAfter) - { - final CompletableFuture> future = new CompletableFuture<>(); - final Future task = executor.submit(new Runnable() - { - @Override - public void run() - { - try { - long bench_start = System.nanoTime(); - ArrayList result = createWidgetListItems(context, contentUri); - long bench_end = System.nanoTime(); - Log.d("WidgetListAdapter", "BENCH: querying " + contentUri + " took " + ((bench_end - bench_start) / 1000000.0) + " ms"); - future.complete(result); - - } catch (Exception e) { - future.completeExceptionally(e); - } - } - }); - - ArrayList result = null; - try { - result = future.get(timeoutAfter, TimeUnit.MILLISECONDS); - - } catch (TimeoutException e) { - Log.e("WidgetListAdapter", "queryDisplayStrings: failed to query AlarmEventItem display strings; request timed out! " + contentUri); - - } catch (InterruptedException | ExecutionException e) { - Log.e("WidgetListAdapter", "queryDisplayStrings: failed to query AlarmEventItem display strings; " + contentUri + ": " + e); - - } finally { - task.cancel(true); - } - return (result != null ? result : new ArrayList()); - } - public static ArrayList createWidgetListItems(@NonNull Context context, @NonNull String contentUri) { if (!contentUri.endsWith("/")) {