diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml new file mode 100644 index 00000000..ddb5d55c --- /dev/null +++ b/.idea/assetWizardSettings.xml @@ -0,0 +1,72 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..7ac24c77 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..37a75096 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..e2f077d6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e5771f98..3cc0b3b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ### ~ +* adds individual "enabled" prefs for each calendar (Solstices/Equinoxes, Moon Phases) (#3). +* fixes bug "missing calendars/events when closing app while task is still running" (#4); moves calendar notifications into a foreground service. +* changes the "Calendar Integration" pref to match calendar state (vs desired state); existing calendars (and prefs) should be preserved when updating (or removing / re-adding) the app. +* changes the strategy used when initializing calendars; existing calendars are no longer cleared prior to (re)adding; subsequent calls to "add" instead "update" a calendar (insert, replace). +* changes when permissions are requested; adds a request on first launch (or if data is cleared) before allowing access to remaining UI; permissions are used to recover calendars from previous installations. +* misc improvements to permissions handling; more robust; support for actions on individual calendars. + ### v0.1.1 (2018-12-12) * fixes broken build (jcenter fails to resolve deps). * updates Android gradle plugin to `com.android.tools.build:gradle:3.0.0` (#2). diff --git a/SuntimesCalendars.iml b/SuntimesCalendars.iml new file mode 100644 index 00000000..ec68be22 --- /dev/null +++ b/SuntimesCalendars.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 00000000..33c7c22f --- /dev/null +++ b/app/app.iml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a8bd8026..419a6965 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,6 +27,13 @@ + + + + + + items = loadItems(this.getIntent(), true); + savePendingItems(this, taskIntent, items); + calendarTaskService.runCalendarTask(context, taskIntent, false, false,null); + + if (mainFragment != null) { SharedPreferences.Editor pref = PreferenceManager.getDefaultSharedPreferences(context).edit(); - pref.putBoolean(SuntimesCalendarSettings.PREF_KEY_CALENDARS_ENABLED, enabled); - pref.apply(); - - if (tmp_calendarPref != null) + for (SuntimesCalendarTask.SuntimesCalendarTaskItem item : items) { - tmp_calendarPref.setChecked(enabled); - tmp_calendarPref = null; + boolean enabled = (item.getAction() == SuntimesCalendarTask.SuntimesCalendarTaskItem.ACTION_UPDATE); + pref.putBoolean(SuntimesCalendarSettings.PREF_KEY_CALENDARS_CALENDAR + item.getCalendar(), enabled); + pref.apply(); + + CheckBoxPreference calendarPref = mainFragment.getCalendarPref(item.getCalendar()); + if (calendarPref != null) { + calendarPref.setChecked(enabled); + } + } + } + } + break; + + case REQUEST_CALENDARS_ENABLED: + case REQUEST_CALENDARS_DISABLED: + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) + { + boolean enabled = requestCode == (REQUEST_CALENDARS_ENABLED); + Intent taskIntent = new Intent(this, SuntimesCalendarSyncService.class); + taskIntent.setAction( !enabled ? SuntimesCalendarTaskService.ACTION_CLEAR_CALENDARS : SuntimesCalendarTaskService.ACTION_UPDATE_CALENDARS ); + + ArrayList items = new ArrayList<>(); + if (enabled) { + items = loadItems(this.getIntent(), true); + } + + savePendingItems(this, taskIntent, items); + calendarTaskService.runCalendarTask(context, taskIntent, !enabled, true,null); + + SharedPreferences.Editor pref = PreferenceManager.getDefaultSharedPreferences(context).edit(); + pref.putBoolean(SuntimesCalendarSettings.PREF_KEY_CALENDARS_ENABLED, enabled); + pref.apply(); + + if (mainFragment != null) + { + CheckBoxPreference calendarsPref = mainFragment.getCalendarsEnabledPref(); + if (calendarsPref != null) { + calendarsPref.setChecked(enabled); } } } @@ -263,75 +412,235 @@ protected void onRestoreInstanceState( Bundle bundle ) needsSuntimesPermissions = bundle.getBoolean("needsSuntimesPermissions"); } + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + /** - * CalendarPrefsFragment + * CalendarPrefsFragmentBase */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static class CalendarPrefsFragment extends PreferenceFragment + public static class CalendarPrefsFragmentBase extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Log.i(TAG, "CalendarPrefsFragment: Arguments: " + getArguments()); - PreferenceManager.setDefaultValues(getActivity(), R.xml.preference_calendars, false); - addPreferencesFromResource(R.xml.preference_calendars); - if (savedInstanceState != null && savedInstanceState.containsKey("providerVersion")) { providerVersion = savedInstanceState.getInt("providerVersion"); } + } + @Override + public void onSaveInstanceState(Bundle outState) + { + super.onSaveInstanceState(outState); + if (providerVersion != null) { + outState.putInt("providerVersion", providerVersion); + } + } + + protected Integer providerVersion = null; + public void setProviderVersion( Integer version ) + { + providerVersion = version; + } + + protected Preference.OnPreferenceClickListener onAboutClick = null; + public void setAboutClickListener( Preference.OnPreferenceClickListener onClick ) + { + onAboutClick = onClick; + } + + protected boolean checkDependencies() + { + return (providerVersion != null && providerVersion >= MIN_PROVIDER_VERSION); + } + + protected void showPermissionRational(final Activity activity, final int requestCode) + { + String permissionMessage = activity.getString(R.string.privacy_permission_calendar); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(activity.getString(R.string.privacy_permissiondialog_title)) + .setMessage(fromHtml(permissionMessage)) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() + { + public void onClick(DialogInterface dialog, int which) + { + ActivityCompat.requestPermissions(activity, new String[] { Manifest.permission.WRITE_CALENDAR }, requestCode); + } + }); + builder.show(); + } + + protected void initAboutDialog() + { Preference aboutPref = findPreference("app_about"); if (aboutPref != null && onAboutClick != null) { aboutPref.setOnPreferenceClickListener(onAboutClick); } + } - final Activity activity = getActivity(); - final CheckBoxPreference calendarsEnabledPref = (CheckBoxPreference) findPreference(SuntimesCalendarSettings.PREF_KEY_CALENDARS_ENABLED); - final Preference.OnPreferenceChangeListener onPreferenceChanged0 = new Preference.OnPreferenceChangeListener() + protected ProgressDialog progressDialog; + protected String progressMessage; + public void updateProgressDialog(String message) + { + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.setMessage(message); + progressMessage = message; + } + } + + protected void initProgressDialog() + { + progressDialog = new ProgressDialog(getActivity()); + progressDialog.setCanceledOnTouchOutside(false); + progressDialog.setIndeterminate(true); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * FirstLaunchFragment + */ + public static class FirstLaunchFragment extends CalendarPrefsFragmentBase + { + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + Log.i(TAG, "FirstLaunchFragment: Arguments: " + getArguments()); + PreferenceManager.setDefaultValues(getActivity(), R.xml.preference_firstlaunch, false); + addPreferencesFromResource(R.xml.preference_firstlaunch); + + Preference aboutPref = findPreference("app_about"); + if (aboutPref != null && onAboutClick != null) { + aboutPref.setOnPreferenceClickListener(onAboutClick); + } + + CheckBoxPreference permissionsPref = (CheckBoxPreference) findPreference(SuntimesCalendarSettings.PREF_KEY_CALENDARS_PERMISSIONS); + permissionsPref.setChecked(false); + permissionsPref.setOnPreferenceChangeListener(onPermissionsPrefChanged); + + if (needsSuntimesPermissions || !checkDependencies()) { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) + if (needsSuntimesPermissions) + showPermissionDeniedMessage(getActivity(), getActivity().getWindow().getDecorView().findViewById(android.R.id.content)); + else showMissingDepsMessage(getActivity(), getActivity().getWindow().getDecorView().findViewById(android.R.id.content)); + } + } + + Preference.OnPreferenceChangeListener onPermissionsPrefChanged = new Preference.OnPreferenceChangeListener() + { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) + { + Activity activity = getActivity(); + boolean checkPrefs = (Boolean)newValue; + if (checkPrefs && activity != null) { - boolean enabled = (Boolean)newValue; - int calendarPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CALENDAR); - if (calendarPermission != PackageManager.PERMISSION_GRANTED) + if (!hasCalendarPermissions(activity)) { - final int requestCode = (enabled ? REQUEST_CALENDARPREFSFRAGMENT_ENABLED : REQUEST_CALENDARPREFSFRAGMENT_DISABLED); - if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_CALENDAR)) - { - String permissionMessage = activity.getString(R.string.privacy_permission_calendar); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(activity.getString(R.string.privacy_permissiondialog_title)) - .setMessage(fromHtml(permissionMessage)) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() - { - public void onClick(DialogInterface dialog, int which) - { - ActivityCompat.requestPermissions(activity, new String[] { Manifest.permission.WRITE_CALENDAR }, requestCode); - tmp_calendarPref = calendarsEnabledPref; - } - }); - - //if (Build.VERSION.SDK_INT >= 11) - //builder.setIconAttribute(R.attr.icActionWarning); - //else builder.setIcon(R.drawable.ic_action_warning); - - builder.show(); - return false; - + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_CALENDAR)) { + showPermissionRational(activity, REQUEST_CALENDAR_FIRSTLAUNCH); } else { - ActivityCompat.requestPermissions(activity, new String[] { Manifest.permission.WRITE_CALENDAR }, requestCode); - tmp_calendarPref = calendarsEnabledPref; - return false; + ActivityCompat.requestPermissions(activity, new String[] { Manifest.permission.WRITE_CALENDAR }, REQUEST_CALENDAR_FIRSTLAUNCH); } } else { - return runCalendarTask(activity, enabled); + SuntimesCalendarSettings.saveFirstLaunch(activity); + activity.recreate(); } } - }; - calendarsEnabledPref.setOnPreferenceChangeListener(onPreferenceChanged0); + return false; + } + }; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * CalendarPrefsFragment + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class CalendarPrefsFragment extends CalendarPrefsFragmentBase + { + private CheckBoxPreference calendarsEnabledPref = null; + public CheckBoxPreference getCalendarsEnabledPref() + { + return calendarsEnabledPref; + } + + private HashMap calendarPrefs = new HashMap<>(); + public CheckBoxPreference getCalendarPref(String calendar) + { + return calendarPrefs.get(calendar); + } + + private boolean isBusy = false; + public void setIsBusy(boolean isBusy) + { + this.isBusy = isBusy; + if (progressDialog != null) + { + if (isBusy) + { + if (!progressDialog.isShowing()) { + progressDialog.show(); + } + + } else { + if (progressDialog.isShowing()) { + progressDialog.dismiss(); + } + clearPrefListeners(); + updatePrefs(getActivity()); + initPrefListeners(getActivity()); + } + } + } + + @Override + public void onStart() + { + super.onStart(); + if (progressDialog != null && isBusy) { + progressDialog.show(); + updateProgressDialog(progressMessage); + } + } + + @Override + public void onStop() + { + super.onStop(); + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + Log.i(TAG, "CalendarPrefsFragment: Arguments: " + getArguments()); + PreferenceManager.setDefaultValues(getActivity(), R.xml.preference_calendars, false); + addPreferencesFromResource(R.xml.preference_calendars); + + final Activity activity = getActivity(); + initAboutDialog(); + initProgressDialog(); + + calendarsEnabledPref = (CheckBoxPreference) findPreference(SuntimesCalendarSettings.PREF_KEY_CALENDARS_ENABLED); + for (String calendar : SuntimesCalendarAdapter.ALL_CALENDARS) + { + CheckBoxPreference calendarPref = (CheckBoxPreference)findPreference(SuntimesCalendarSettings.PREF_KEY_CALENDARS_CALENDAR + calendar); + calendarPrefs.put(calendar, calendarPref); + } + + updatePrefs(activity); + initPrefListeners(activity); if (needsSuntimesPermissions || !checkDependencies()) { @@ -348,35 +657,177 @@ public void onClick(DialogInterface dialog, int which) showPermissionDeniedMessage(getActivity(), getActivity().getWindow().getDecorView().findViewById(android.R.id.content)); else showMissingDepsMessage(getActivity(), getActivity().getWindow().getDecorView().findViewById(android.R.id.content)); } + setIsBusy(isBusy); } - @Override - public void onSaveInstanceState(Bundle outState) + private void initPrefListeners(Activity activity) { - super.onSaveInstanceState(outState); - if (providerVersion != null) { - outState.putInt("providerVersion", providerVersion); + if (activity == null) + return; + + calendarsEnabledPref.setOnPreferenceChangeListener(onPreferenceChanged0(activity)); + for (String calendar : calendarPrefs.keySet()) + { + CheckBoxPreference calendarPref = calendarPrefs.get(calendar); + calendarPref.setOnPreferenceChangeListener(onPreferenceChanged1(activity, calendar)); } } - private Integer providerVersion = null; - public void setProviderVersion( Integer version ) + private void clearPrefListeners() { - providerVersion = version; + calendarsEnabledPref.setOnPreferenceChangeListener(null); + for (String calendar : calendarPrefs.keySet()) + { + CheckBoxPreference calendarPref = calendarPrefs.get(calendar); + calendarPref.setOnPreferenceChangeListener(null); + } } - private Preference.OnPreferenceClickListener onAboutClick = null; - public void setAboutClickListener( Preference.OnPreferenceClickListener onClick ) + private void updatePrefs(Activity activity) { - onAboutClick = onClick; + if (activity == null) + return; + + SuntimesCalendarAdapter adapter = new SuntimesCalendarAdapter(activity.getContentResolver()); + SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(activity).edit(); + + if (hasCalendarPermissions(activity)) + { + boolean calendarsEnabled0 = adapter.hasCalendars(); + boolean calendarsEnabled1 = calendarsEnabledPref.isChecked(); + if (calendarsEnabled0 != calendarsEnabled1) + { + Log.w(TAG, "onCreate: out of sync! setting pref to " + (calendarsEnabled0 ? "enabled" : "disabled")); + prefs.putBoolean(SuntimesCalendarSettings.PREF_KEY_CALENDARS_ENABLED, calendarsEnabled0); + prefs.apply(); + calendarsEnabledPref.setChecked(calendarsEnabled0); + } + + for (String calendar : calendarPrefs.keySet()) + { + CheckBoxPreference calendarPref = calendarPrefs.get(calendar); + if (calendarsEnabledPref.isChecked()) + { + boolean enabled0 = adapter.hasCalendar(calendar); + boolean enabled1 = SuntimesCalendarSettings.loadPrefCalendarEnabled(activity, calendar); + if (enabled0 != enabled1) + { + Log.w(TAG, "onCreate: out of sync! setting " + calendar + " to " + (enabled0 ? "enabled" : "disabled")); + prefs.putBoolean(SuntimesCalendarSettings.PREF_KEY_CALENDARS_CALENDAR + calendar, enabled0); + prefs.apply(); + calendarPref.setChecked(enabled0); + } + } + } + } } - protected boolean checkDependencies() + private Preference.OnPreferenceChangeListener onPreferenceChanged0(final Activity activity) { - return (providerVersion != null && providerVersion >= MIN_PROVIDER_VERSION); + return new Preference.OnPreferenceChangeListener() + { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) + { + boolean enabled = (Boolean)newValue; + if (!hasCalendarPermissions(activity)) + { + final int requestCode = (enabled ? REQUEST_CALENDARS_ENABLED : REQUEST_CALENDARS_DISABLED); + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_CALENDAR)) + { + if (enabled) { + savePendingItems(activity, activity.getIntent()); + } + showPermissionRational(activity, requestCode); + return false; + + } else { + if (enabled) { + savePendingItems(activity, activity.getIntent()); + } + ActivityCompat.requestPermissions(activity, new String[] { Manifest.permission.WRITE_CALENDAR }, requestCode); + return false; + } + + } else { + Intent taskIntent = new Intent(getActivity(), SuntimesCalendarSyncService.class); + taskIntent.setAction( !enabled ? SuntimesCalendarTaskService.ACTION_CLEAR_CALENDARS : SuntimesCalendarTaskService.ACTION_UPDATE_CALENDARS ); + savePendingItems(activity, taskIntent); + return calendarTaskService.runCalendarTask(activity, taskIntent, !enabled, true,null); + } + } + }; + } + + private Preference.OnPreferenceChangeListener onPreferenceChanged1(final Activity activity, final String calendar) + { + return new Preference.OnPreferenceChangeListener() + { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) + { + boolean calendarsEnabled = SuntimesCalendarSettings.loadCalendarsEnabledPref(activity); + if (calendarsEnabled) + { + boolean enabled = (Boolean)newValue; + if (!hasCalendarPermissions(activity)) + { + final int requestCode = (enabled ? REQUEST_CALENDAR_ENABLED : REQUEST_CALENDAR_DISABLED); + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_CALENDAR)) + { + savePendingItem(activity, activity.getIntent(), calendar, enabled); + showPermissionRational(activity, requestCode); + return false; + + } else { + savePendingItem(activity, activity.getIntent(), calendar, enabled); + ActivityCompat.requestPermissions(activity, new String[] { Manifest.permission.WRITE_CALENDAR }, requestCode); + return false; + } + + } else { + Intent taskIntent = new Intent(getActivity(), SuntimesCalendarSyncService.class); + taskIntent.setAction( SuntimesCalendarTaskService.ACTION_UPDATE_CALENDARS ); + savePendingItem(activity, taskIntent, calendar, enabled); + return calendarTaskService.runCalendarTask(activity, taskIntent, false, true,null); + } + + } else { + return true; + } + } + }; } } + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + + /**private static class OnServiceResponse extends SuntimesCalendarSyncService.SuntimesCalendarServiceListener + { + @Override + public void onStartCommand(boolean result) + { + super.onStartCommand(result); + } + + public OnServiceResponse() { + super(); + } + public OnServiceResponse(Parcel in) { + super(in); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public OnServiceResponse createFromParcel(Parcel in) { + return new OnServiceResponse(in); + } + public OnServiceResponse[] newArray(int size) { + return new OnServiceResponse[size]; + } + }; + }*/ + protected static void showMissingDepsMessage(final Activity context, View view) { if (view != null) @@ -439,98 +890,48 @@ public void onClick(View v) } } - - private static CheckBoxPreference tmp_calendarPref = null; - private static SuntimesCalendarTask calendarTask = null; - private static boolean runCalendarTask(final Activity activity, boolean enabled) + /** + * loadItems + */ + public static ArrayList loadItems(Intent intent, boolean clearPending) { - if (calendarTask != null) - { - switch (calendarTask.getStatus()) - { - case PENDING: - Log.w(TAG, "runCalendarTask: A task is already pending! ignoring..."); - return false; - - case RUNNING: - Log.w(TAG, "runCalendarTask: A task is already running! ignoring..."); - return false; - } + SuntimesCalendarTask.SuntimesCalendarTaskItem[] items; + Parcelable[] parcelableArray = intent.getParcelableArrayExtra(SuntimesCalendarTaskService.EXTRA_CALENDAR_ITEMS); + if (parcelableArray != null) { + items = Arrays.copyOf(parcelableArray, parcelableArray.length, SuntimesCalendarTask.SuntimesCalendarTaskItem[].class); + } else items = new SuntimesCalendarTask.SuntimesCalendarTaskItem[0]; + + if (clearPending) { + intent.removeExtra(SuntimesCalendarTaskService.EXTRA_CALENDAR_ITEMS); } + return new ArrayList<>(Arrays.asList(items)); + } - calendarTask = new SuntimesCalendarTask(activity); - if (!enabled) { - calendarTask.setFlagClearCalendars(true); - } - calendarTask.setTaskListener(new SuntimesCalendarTask.SuntimesCalendarTaskListener() - { - private ProgressDialog progress; - - @Override - public void onStarted(boolean flag_clear) - { - if (!flag_clear) - { - //Toast.makeText(activity, activity.getString(R.string.calendars_notification_adding), Toast.LENGTH_SHORT).show(); - - progress = new ProgressDialog(activity); - progress.setIndeterminate(true); - progress.setMessage(activity.getString(R.string.calendars_notification_adding)); - progress.setCanceledOnTouchOutside(false); - progress.show(); - } - } - - @Override - public void onSuccess(boolean flag_clear) - { - if (progress != null) { - progress.dismiss(); - } - - if (!flag_clear) - Toast.makeText(activity, activity.getString(R.string.calendars_notification_added), Toast.LENGTH_SHORT).show(); - else Toast.makeText(activity, activity.getString(R.string.calendars_notification_cleared), Toast.LENGTH_SHORT).show(); - } - - @Override - public void onFailed(final String errorMsg) - { - if (progress != null) { - progress.dismiss(); - } - - super.onFailed(errorMsg); - AlertDialog.Builder errorDialog = new AlertDialog.Builder(activity); - errorDialog.setTitle(activity.getString(R.string.calendars_notification_adding_failed)) - .setMessage(errorMsg) - .setIcon(R.drawable.ic_action_about) - .setNeutralButton(activity.getString(R.string.actionCopyError), new DialogInterface.OnClickListener() - { - public void onClick(DialogInterface dialog, int which) - { - ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); - if (clipboard != null) { - ClipData clip = ClipData.newPlainText("SuntimesCalendarErrorMsg", errorMsg); - clipboard.setPrimaryClip(clip); - Toast.makeText(activity, activity.getString(R.string.actionCopyError_toast), Toast.LENGTH_SHORT).show(); - } - } - }) - .setPositiveButton(android.R.string.ok, null); - errorDialog.show(); + /** + * saveItems + */ + public static void savePendingItems(Activity activity, Intent intent) + { + ArrayList items = new ArrayList<>(); + for (String calendar : SuntimesCalendarAdapter.ALL_CALENDARS) { + if (SuntimesCalendarSettings.loadPrefCalendarEnabled(activity, calendar)) { + items.add(new SuntimesCalendarTask.SuntimesCalendarTaskItem(calendar, SuntimesCalendarTask.SuntimesCalendarTaskItem.ACTION_UPDATE)); } - }); + } + savePendingItems(activity, intent, items); + } - calendarTask.execute(); - return true; + public static void savePendingItems(Activity activity, Intent intent, ArrayList items) + { + intent.putExtra(SuntimesCalendarTaskService.EXTRA_CALENDAR_ITEMS, items.toArray(new SuntimesCalendarTask.SuntimesCalendarTaskItem[0])); } - public static Spanned fromHtml(String htmlString ) + public static void savePendingItem(Activity activity, Intent intent, String calendar, boolean enabled) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - return Html.fromHtml(htmlString, Html.FROM_HTML_MODE_LEGACY); - else return Html.fromHtml(htmlString); + ArrayList items = new ArrayList<>(); + int action = (enabled ? SuntimesCalendarTask.SuntimesCalendarTaskItem.ACTION_UPDATE : SuntimesCalendarTask.SuntimesCalendarTaskItem.ACTION_DELETE); + items.add(new SuntimesCalendarTask.SuntimesCalendarTaskItem(calendar, action)); + savePendingItems(activity, intent, items); } /** @@ -552,4 +953,11 @@ public boolean onPreferenceClick(Preference preference) return false; } }; + + public static Spanned fromHtml(String htmlString ) + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + return Html.fromHtml(htmlString, Html.FROM_HTML_MODE_LEGACY); + else return Html.fromHtml(htmlString); + } } diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarAdapter.java b/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarAdapter.java index aeec119a..46aa9b65 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarAdapter.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarAdapter.java @@ -37,6 +37,7 @@ public class SuntimesCalendarAdapter public static final String CALENDAR_SOLSTICE = "solsticeCalendar"; public static final String CALENDAR_MOONPHASE = "moonPhaseCalendar"; + public static final String[] ALL_CALENDARS = new String[] {CALENDAR_SOLSTICE, CALENDAR_MOONPHASE}; private ContentResolver contentResolver; @@ -77,6 +78,23 @@ public boolean removeCalendars() } else return false; } + /** + * Removes individual calendars by name. + * @param calendar calendar name + * @return true calendar was removed, false otherwise + */ + public boolean removeCalendar(String calendar) + { + long calendarID = queryCalendarID(calendar); + if (calendarID != -1) + { + Uri deleteUri = ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarID); + contentResolver.delete(deleteUri, null, null); + Log.d(TAG, "removeCalendar: removed calendar " + calendarID); + return true; + } else return false; + } + /** * @param calendarID the calendar's ID * @param title the event title @@ -146,6 +164,30 @@ public boolean hasCalendar(String calendarName) return (cursor != null && cursor.getCount() > 0); } + /** + * @return true if any calendars are being managed by the "Suntimes" local account, false no calendars exist. + */ + public boolean hasCalendars() + { + try { + for (String calendar : ALL_CALENDARS) + { + Cursor cursor = queryCalendar(calendar); + if (cursor != null) + { + boolean hasCalendar = (cursor.getCount() > 0); + cursor.close(); + if (hasCalendar) { + return true; + } + } + } + } catch (SecurityException e) { + return false; + } + return false; + } + /** * @param calendarName * @param displayName diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarErrorActivity.java b/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarErrorActivity.java new file mode 100644 index 00000000..e37d6e4b --- /dev/null +++ b/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarErrorActivity.java @@ -0,0 +1,105 @@ +/* + Copyright (C) 2018 Forrest Guice + This file is part of SuntimesCalendars. + + SuntimesCalendars 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. + + SuntimesCalendars 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 SuntimesCalendars. If not, see . +*/ + +package com.forrestguice.suntimeswidget.calendar; + +import android.app.Dialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; + +import com.forrestguice.suntimescalendars.R; + +public class SuntimesCalendarErrorActivity extends AppCompatActivity +{ + public static final String EXTRA_ERROR_MESSAGE = "calendar_error"; + + public SuntimesCalendarErrorActivity() + { + super(); + } + + @Override + public void onCreate(Bundle icicle) + { + setResult(RESULT_OK); + super.onCreate(icicle); + + final Context context = this; + final String errorMsg = getIntent().getStringExtra(EXTRA_ERROR_MESSAGE); + + AlertDialog.Builder errorDialog = new AlertDialog.Builder(context); + errorDialog.setTitle(context.getString(R.string.calendars_notification_adding_failed)) + .setMessage(errorMsg) + .setIcon(R.drawable.ic_action_about) + .setNeutralButton(context.getString(R.string.actionCopyError), null) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) + { + SuntimesCalendarErrorActivity.this.finish(); + overridePendingTransition(0, 0); + } + }) + .setPositiveButton(android.R.string.ok, null); + + Dialog dialog = errorDialog.create(); + dialog.setCanceledOnTouchOutside(false); + dialog.setOnShowListener(new DialogInterface.OnShowListener() + { + @Override + public void onShow(DialogInterface dialog) + { + Button neutralButton = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEUTRAL); + neutralButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) + { + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard != null) { + ClipData clip = ClipData.newPlainText("SuntimesCalendarErrorMsg", errorMsg); + clipboard.setPrimaryClip(clip); + Toast.makeText(context, context.getString(R.string.actionCopyError_toast), Toast.LENGTH_SHORT).show(); + } + } + }); + } + }); + dialog.show(); + } + + @Override + protected void onSaveInstanceState( Bundle bundle ) + { + super.onSaveInstanceState(bundle); + } + + @Override + protected void onRestoreInstanceState( Bundle bundle ) + { + super.onRestoreInstanceState(bundle); + } + +} diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarSettings.java b/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarSettings.java index 7f9beefd..dc87ffba 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarSettings.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarSettings.java @@ -33,6 +33,31 @@ public class SuntimesCalendarSettings public static final String PREF_KEY_CALENDAR_WINDOW1 = "app_calendars_window1"; public static final String PREF_DEF_CALENDAR_WINDOW1 = "63072000000"; // 2 years + public static final String PREF_KEY_CALENDARS_CALENDAR = "app_calendars_calendar_"; + + public static final String PREF_KEY_CALENDARS_FIRSTLAUNCH = "app_calendars_firstlaunch"; + public static final String PREF_KEY_CALENDARS_PERMISSIONS = "app_calendars_permissions"; + + /** + * @param context + * @return + */ + public static boolean isFirstLaunch(Context context) + { + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); + return pref.getBoolean(PREF_KEY_CALENDARS_FIRSTLAUNCH, true); + } + + /** + * @param context + */ + public static void saveFirstLaunch(Context context) + { + SharedPreferences.Editor pref = PreferenceManager.getDefaultSharedPreferences(context).edit(); + pref.putBoolean(PREF_KEY_CALENDARS_FIRSTLAUNCH, false); + pref.apply(); + } + /** * @param context * @param enabled @@ -73,4 +98,14 @@ public static long loadPrefCalendarWindow1(Context context) SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); return Long.parseLong(prefs.getString(PREF_KEY_CALENDAR_WINDOW1, PREF_DEF_CALENDAR_WINDOW1)); } + + /** + * @param context context used to access preferences + * @return true calendar is enabled, false otherwise + */ + public static boolean loadPrefCalendarEnabled(Context context, String calendar) + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getBoolean(PREF_KEY_CALENDARS_CALENDAR + calendar, false); + } } diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarSyncService.java b/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarSyncService.java index 438f6411..baf427ce 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarSyncService.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarSyncService.java @@ -21,6 +21,7 @@ import android.app.Service; import android.content.Intent; import android.os.IBinder; + import android.support.annotation.Nullable; public class SuntimesCalendarSyncService extends Service @@ -28,6 +29,13 @@ public class SuntimesCalendarSyncService extends Service private static SuntimesCalendarSyncAdapter syncAdapter = null; private static final Object syncAdapterLock = new Object(); + @Nullable + @Override + public IBinder onBind(Intent intent) + { + return syncAdapter.getSyncAdapterBinder(); + } + @Override public void onCreate() { @@ -39,11 +47,4 @@ public void onCreate() } } } - - @Nullable - @Override - public IBinder onBind(Intent intent) - { - return syncAdapter.getSyncAdapterBinder(); - } } diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarTask.java b/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarTask.java index 0c90f9d6..bfbf7bdd 100644 --- a/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarTask.java +++ b/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarTask.java @@ -18,20 +18,16 @@ package com.forrestguice.suntimeswidget.calendar; -import android.app.Activity; -import android.app.PendingIntent; import android.content.ContentResolver; -import android.content.ContentUris; import android.content.Context; -import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; -import android.provider.CalendarContract; -import android.support.v4.app.NotificationManagerCompat; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; -import android.support.v7.app.NotificationCompat; import android.util.Log; import com.forrestguice.suntimescalendars.R; @@ -41,13 +37,14 @@ import java.util.Calendar; import java.util.HashMap; -public class SuntimesCalendarTask extends AsyncTask +public class SuntimesCalendarTask extends AsyncTask { public static final String TAG = "SuntimesCalendarTask"; private SuntimesCalendarAdapter adapter; private WeakReference contextRef; + private HashMap calendars = new HashMap<>(); private HashMap calendarDisplay = new HashMap<>(); private HashMap calendarColors = new HashMap<>(); @@ -58,20 +55,12 @@ public class SuntimesCalendarTask extends AsyncTask private long lastSync = -1; private long calendarWindow0 = -1, calendarWindow1 = -1; - private NotificationManagerCompat notificationManager; - private NotificationCompat.Builder notificationBuilder; - - public static final int NOTIFICATION_ID = 1000; - private String notificationTitle; private String notificationMsgAdding, notificationMsgAdded; private String notificationMsgClearing, notificationMsgCleared; private String notificationMsgAddFailed; - private int notificationIcon = R.drawable.ic_action_calendar; - private int notificationPriority = NotificationCompat.PRIORITY_LOW; - private PendingIntent notificationIntent; private String lastError = null; - public SuntimesCalendarTask(Activity context) + public SuntimesCalendarTask(Context context) { contextRef = new WeakReference(context); adapter = new SuntimesCalendarAdapter(context.getContentResolver()); @@ -101,30 +90,11 @@ public SuntimesCalendarTask(Activity context) calendarDisplay.put(SuntimesCalendarAdapter.CALENDAR_MOONPHASE, context.getString(R.string.calendar_moonPhase_displayName)); calendarColors.put(SuntimesCalendarAdapter.CALENDAR_MOONPHASE, ContextCompat.getColor(context, R.color.colorMoonCalendar)); - notificationManager = NotificationManagerCompat.from(context); - notificationBuilder = new NotificationCompat.Builder(context); - notificationTitle = context.getString(R.string.app_name); notificationMsgAdding = context.getString(R.string.calendars_notification_adding); notificationMsgAdded = context.getString(R.string.calendars_notification_added); notificationMsgClearing = context.getString(R.string.calendars_notification_clearing); notificationMsgCleared = context.getString(R.string.calendars_notification_cleared); notificationMsgAddFailed = context.getString(R.string.calendars_notification_adding_failed); - - Intent intent = new Intent(Intent.ACTION_VIEW); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) - { - Uri.Builder uriBuilder = CalendarContract.CONTENT_URI.buildUpon(); - uriBuilder.appendPath("time"); - ContentUris.appendId(uriBuilder, System.currentTimeMillis()); - intent = intent.setData(uriBuilder.build()); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - } - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - notificationIntent = PendingIntent.getActivity(context, 0, intent, 0); } private boolean flag_notifications = true; @@ -133,6 +103,28 @@ public void setFlagClearCalendars( boolean flag ) { flag_clear = flag; } + public boolean getFlagClearCalendars() + { + return flag_clear; + } + + public void setItems(SuntimesCalendarTaskItem... items) + { + calendars.clear(); + for (SuntimesCalendarTaskItem item : items) { + calendars.put(item.getCalendar(), item); + } + } + public SuntimesCalendarTaskItem[] getItems() { + return calendars.values().toArray(new SuntimesCalendarTaskItem[0]); + } + + public void addItems(SuntimesCalendarTaskItem... items) + { + for (SuntimesCalendarTaskItem item : items) { + calendars.put(item.getCalendar(), item); // TODO: preserve existing + } + } @Override protected void onCancelled () @@ -150,105 +142,156 @@ protected void onPreExecute() } lastError = null; - if (flag_notifications) { - notificationBuilder.setContentTitle(notificationTitle) - .setContentText((flag_clear ? notificationMsgClearing : notificationMsgAdding)) - .setSmallIcon(notificationIcon) - .setPriority(notificationPriority) - .setContentIntent(null).setAutoCancel(false) - .setProgress(0, 0, true); - notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); - } + String message = ""; + if (flag_clear) { + message = notificationMsgClearing; + triggerOnStarted(message); - if (listener != null) { - listener.onStarted(flag_clear); + } else { + SuntimesCalendarTaskItem[] items = calendars.values().toArray(new SuntimesCalendarTask.SuntimesCalendarTaskItem[0]); + if (items.length > 0) { + int action = items[0].getAction(); + message = (action == SuntimesCalendarTaskItem.ACTION_DELETE) ? notificationMsgClearing : notificationMsgAdding; + + if (action != SuntimesCalendarTaskItem.ACTION_DELETE) { + triggerOnStarted(message); + } else triggerOnStarted(""); + } else triggerOnStarted(""); } } + private Calendar[] getWindow() + { + Calendar startDate = Calendar.getInstance(); + Calendar endDate = Calendar.getInstance(); + Calendar now = Calendar.getInstance(); + + startDate.setTimeInMillis(now.getTimeInMillis() - calendarWindow0); + startDate.set(Calendar.MONTH, 0); // round down to start of year + startDate.set(Calendar.DAY_OF_MONTH, 0); + startDate.set(Calendar.HOUR_OF_DAY, 0); + startDate.set(Calendar.MINUTE, 0); + startDate.set(Calendar.SECOND, 0); + + endDate.setTimeInMillis(now.getTimeInMillis() + calendarWindow1); + endDate.add(Calendar.YEAR, 1); // round up to end of year + endDate.set(Calendar.MONTH, 0); + endDate.set(Calendar.DAY_OF_MONTH, 0); + endDate.set(Calendar.HOUR_OF_DAY, 0); + endDate.set(Calendar.MINUTE, 0); + endDate.set(Calendar.SECOND, 0); + + return new Calendar[] { startDate, endDate }; + } + @Override - protected Boolean doInBackground(Void... params) + protected Boolean doInBackground(SuntimesCalendarTaskItem... items) { if (Build.VERSION.SDK_INT < 14) return false; - boolean retValue = adapter.removeCalendars(); - if (!flag_clear && !isCancelled()) - { - Calendar startDate = Calendar.getInstance(); - Calendar endDate = Calendar.getInstance(); - Calendar now = Calendar.getInstance(); - - startDate.setTimeInMillis(now.getTimeInMillis() - calendarWindow0); - startDate.set(Calendar.MONTH, 0); // round down to start of year - startDate.set(Calendar.DAY_OF_MONTH, 0); - startDate.set(Calendar.HOUR_OF_DAY, 0); - startDate.set(Calendar.MINUTE, 0); - startDate.set(Calendar.SECOND, 0); - - endDate.setTimeInMillis(now.getTimeInMillis() + calendarWindow1); - endDate.add(Calendar.YEAR, 1); // round up to end of year - endDate.set(Calendar.MONTH, 0); - endDate.set(Calendar.DAY_OF_MONTH, 0); - endDate.set(Calendar.HOUR_OF_DAY, 0); - endDate.set(Calendar.MINUTE, 0); - endDate.set(Calendar.SECOND, 0); - - Log.d(TAG, "Adding... startWindow: " + calendarWindow0 + " (" + startDate.get(Calendar.YEAR) + "), " - + "endWindow: " + calendarWindow1 + " (" + endDate.get(Calendar.YEAR) + ")"); - - try { - retValue = retValue && initSolsticeCalendar(startDate, endDate); - retValue = retValue && initMoonPhaseCalendar(startDate, endDate); - - } catch (SecurityException e) { - lastError = "Unable to access provider! " + e; - Log.e(TAG, lastError); - return false; + if (items.length > 0) { + setItems(items); + } + + boolean retValue = true; + + if (flag_clear && !isCancelled()) { + adapter.removeCalendars(); + } + + Calendar[] window = getWindow(); + Log.d(TAG, "Adding... startWindow: " + calendarWindow0 + " (" + window[0].get(Calendar.YEAR) + "), " + + "endWindow: " + calendarWindow1 + " (" + window[1].get(Calendar.YEAR) + ")"); + + try { + for (String calendar : calendars.keySet()) + { + SuntimesCalendarTaskItem item = calendars.get(calendar); + switch (item.getAction()) + { + case SuntimesCalendarTaskItem.ACTION_DELETE: + onProgressUpdate(notificationMsgClearing); + retValue = retValue && adapter.removeCalendar(calendar); + break; + + case SuntimesCalendarTaskItem.ACTION_UPDATE: + default: + onProgressUpdate(notificationMsgAdding); + retValue = retValue && initCalendar(calendar, window); + break; + } } + + } catch (SecurityException e) { + lastError = "Unable to access provider! " + e; + Log.e(TAG, lastError); + return false; } + return retValue; } @Override protected void onPostExecute(Boolean result) { + Context context = contextRef.get(); if (result) { - Context context = contextRef.get(); if (context != null) { SuntimesCalendarSyncAdapter.writeLastSyncTime(context, Calendar.getInstance()); } - if (flag_notifications) { - notificationBuilder.setContentTitle(notificationTitle) - .setContentText((flag_clear ? notificationMsgCleared : notificationMsgAdded)) - .setSmallIcon(notificationIcon) - .setPriority(notificationPriority) - .setContentIntent(notificationIntent).setAutoCancel(true) - .setProgress(0, 0, false); - notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + String message = (flag_clear ? notificationMsgCleared : notificationMsgAdded); + SuntimesCalendarTaskItem[] items = calendars.values().toArray(new SuntimesCalendarTask.SuntimesCalendarTaskItem[0]); + if (items.length > 0) { + if (items[0].getAction() == SuntimesCalendarTaskItem.ACTION_DELETE) { + message = notificationMsgCleared; + } } - if (flag_clear) - Log.i(TAG, "Cleared Suntimes Calendars..."); - else Log.i(TAG, "Added Suntimes Calendars..."); - - if (listener != null) { - listener.onSuccess(flag_clear); + if (listener != null && context != null) { + listener.onSuccess(context, this, message); } } else { Log.w(TAG, "Failed to complete task!"); - notificationManager.cancel(NOTIFICATION_ID); - if (listener != null) { - listener.onFailed(lastError); + if (listener != null && context != null) { + listener.onFailed(context, lastError); } } } - private static final int NOTIFICATION_REQUEST_LASTERROR = 10; + /** + * initCalendar + */ + private boolean initCalendar(@NonNull String calendar, @NonNull Calendar[] window) throws SecurityException + { + if (window.length != 2) { + Log.e(TAG, "initCalendar: invalid window with length " + window.length); + return false; + + } else if (window[0] == null || window[1] == null) { + Log.e(TAG, "initCalendar: invalid window; null!"); + return false; + } + + if (calendar.equals(SuntimesCalendarAdapter.CALENDAR_SOLSTICE)) { + return initSolsticeCalendar(window[0], window[1]); + + } else if (calendar.equals(SuntimesCalendarAdapter.CALENDAR_MOONPHASE)) { + return initMoonPhaseCalendar(window[0], window[1]); + + } else { + Log.w(TAG, "initCalendar: unrecognized calendar " + calendar); + return false; + } + } - private boolean initSolsticeCalendar( Calendar startDate, Calendar endDate ) throws SecurityException + /** + * initSolsticeCalendar + */ + private boolean initSolsticeCalendar(@NonNull Calendar startDate, @NonNull Calendar endDate ) throws SecurityException { if (isCancelled()) { return false; @@ -298,7 +341,10 @@ private boolean initSolsticeCalendar( Calendar startDate, Calendar endDate ) thr } else return false; } - private boolean initMoonPhaseCalendar( Calendar startDate, Calendar endDate ) throws SecurityException + /** + * initMoonPhaseCalendar + */ + private boolean initMoonPhaseCalendar( @NonNull Calendar startDate, @NonNull Calendar endDate ) throws SecurityException { if (isCancelled()) { return false; @@ -353,17 +399,99 @@ private boolean initMoonPhaseCalendar( Calendar startDate, Calendar endDate ) th } else return false; } + /** + * SuntimesCalendarTaskItem + */ + public static class SuntimesCalendarTaskItem implements Parcelable + { + public static final int ACTION_UPDATE = 0; + public static final int ACTION_DELETE = 2; + + private String calendar; + private int action; + + public SuntimesCalendarTaskItem( String calendar, int action ) + { + this.calendar = calendar; + this.action = action; + } + + private SuntimesCalendarTaskItem(Parcel in) + { + this.calendar = in.readString(); + this.action = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) + { + dest.writeString(calendar); + dest.writeInt(action); + } + + @Override + public int describeContents() + { + return 0; + } + + public String getCalendar() + { + return calendar; + } + + public int getAction() + { + return action; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() + { + public SuntimesCalendarTaskItem createFromParcel(Parcel in) + { + return new SuntimesCalendarTaskItem(in); + } + + public SuntimesCalendarTaskItem[] newArray(int size) + { + return new SuntimesCalendarTaskItem[size]; + } + }; + } + + /** + * SuntimesCalendarTaskListener + */ + public static abstract class SuntimesCalendarTaskListener implements Parcelable + { + public void onStarted(Context context, SuntimesCalendarTask task, String message) {} + public void onSuccess(Context context, SuntimesCalendarTask task, String message) {} + public void onFailed(Context context, String errorMsg) {} + + public SuntimesCalendarTaskListener() {} + + protected SuntimesCalendarTaskListener(Parcel in) {} + + @Override + public void writeToParcel(Parcel dest, int flags) {} + + @Override + public int describeContents() { + return 0; + } + } + private SuntimesCalendarTaskListener listener; public void setTaskListener( SuntimesCalendarTaskListener listener ) { this.listener = listener; - this.listener = listener; } - public static abstract class SuntimesCalendarTaskListener + protected void triggerOnStarted(String message) { - public void onStarted(boolean flag_clear) {} - public void onSuccess(boolean flag_cleared) {} - public void onFailed(String errorMsg) {} + Context context = contextRef.get(); + if (listener != null && context != null) { + listener.onStarted(context, this, message); + } } } diff --git a/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarTaskService.java b/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarTaskService.java new file mode 100644 index 00000000..36cf96f1 --- /dev/null +++ b/app/src/main/java/com/forrestguice/suntimeswidget/calendar/SuntimesCalendarTaskService.java @@ -0,0 +1,315 @@ +/** + Copyright (C) 2018 Forrest Guice + This file is part of SuntimesCalendars. + + SuntimesCalendars 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. + + SuntimesCalendars 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 SuntimesCalendars. If not, see . +*/ + +package com.forrestguice.suntimeswidget.calendar; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.CalendarContract; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v7.app.NotificationCompat; +import android.util.Log; +import android.widget.Toast; + +import com.forrestguice.suntimescalendars.R; + +import java.util.ArrayList; +import java.util.Arrays; + +public class SuntimesCalendarTaskService extends Service +{ + public static final String TAG = "SuntimesCalendarsTask"; + public static final String ACTION_UPDATE_CALENDARS = "update_calendars"; + public static final String ACTION_CLEAR_CALENDARS = "clear_calendars"; + + public static final String EXTRA_CALENDAR_ITEMS = "calendar_items"; + public static final String EXTRA_CALENDAR_LISTENER = "calendar_listener"; + public static final String EXTRA_SERVICE_LISTENER = "service_listener"; + + @Nullable + @Override + public IBinder onBind(Intent intent) + { + return taskBinder; + } + + private final SuntimesCalendarTaskServiceBinder taskBinder = new SuntimesCalendarTaskServiceBinder(); + public class SuntimesCalendarTaskServiceBinder extends Binder + { + SuntimesCalendarTaskService getService() { + return SuntimesCalendarTaskService.this; + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) + { + String action = intent.getAction(); + if (action != null) + { + SuntimesCalendarServiceListener serviceListener = intent.getParcelableExtra(EXTRA_SERVICE_LISTENER); + SuntimesCalendarTask.SuntimesCalendarTaskListener listener = intent.getParcelableExtra(EXTRA_CALENDAR_LISTENER); + if (action.equals(ACTION_UPDATE_CALENDARS)) + { + Log.d(TAG, "onStartCommand: " + action); + boolean started = runCalendarTask(this, intent, false, false, listener); + signalOnStartCommand(started); + if (serviceListener != null) { + serviceListener.onStartCommand(started); + } + + } else if (action.equals(ACTION_CLEAR_CALENDARS)) { + Log.d(TAG, "onStartCommand: " + action); + boolean started = runCalendarTask(this, intent, true, false, listener); + signalOnStartCommand(started); + if (serviceListener != null) { + serviceListener.onStartCommand(started); + } + + } else Log.d(TAG, "onStartCommand: unrecognized action: " + action); + } else Log.d(TAG, "onStartCommand: null action"); + return START_NOT_STICKY; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + + public static final int NOTIFICATION_PROGRESS = 10; + public static final int NOTIFICATION_COMPLETE = 20; + + private static SuntimesCalendarTask calendarTask = null; + private static SuntimesCalendarTask.SuntimesCalendarTaskListener calendarTaskListener; + public boolean runCalendarTask(final Context context, Intent intent, boolean clearCalendars, boolean clearPending, @Nullable final SuntimesCalendarTask.SuntimesCalendarTaskListener listener) + { + ArrayList items = new ArrayList<>(); + if (!clearCalendars) { + items = loadItems(intent, clearPending); + } + + if (isBusy()) { + Log.w(TAG, "runCalendarTask: A task is already running! ignoring..."); + return false; + } + + calendarTask = new SuntimesCalendarTask(context); + calendarTaskListener = (listener != null) ? listener : new SuntimesCalendarTask.SuntimesCalendarTaskListener() + { + @Override + public void onStarted(Context context, SuntimesCalendarTask task, String message) + { + if (!task.getFlagClearCalendars() && hasUpdateAction(task.getItems())) + { + signalOnBusyStatusChanged(true); + signalOnProgressMessage(getString(R.string.calendars_notification_adding)); + + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context); + notificationBuilder.setContentTitle(context.getString(R.string.app_name)) + .setContentText(message) + .setSmallIcon(R.drawable.ic_action_update) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setProgress(0, 0, true); + startService(new Intent( context, SuntimesCalendarTaskService.class )); // bind the service to itself (to keep things running if the activity unbinds) + startForeground(NOTIFICATION_PROGRESS, notificationBuilder.build()); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.cancel(NOTIFICATION_COMPLETE); + } + } + + private boolean hasUpdateAction(SuntimesCalendarTask.SuntimesCalendarTaskItem[] items) + { + for (SuntimesCalendarTask.SuntimesCalendarTaskItem item : items) { + if (item.getAction() == SuntimesCalendarTask.SuntimesCalendarTaskItem.ACTION_UPDATE) { + return true; + } + } + return false; + } + + @Override + public void onSuccess(Context context, SuntimesCalendarTask task, String message) + { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context); + notificationBuilder.setContentTitle(context.getString(R.string.app_name)) + .setContentText(message) + .setSmallIcon(R.drawable.ic_action_calendar) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setContentIntent(getNotificationIntent()).setAutoCancel(true) + .setProgress(0, 0, false); + + notificationManager.notify(NOTIFICATION_COMPLETE, notificationBuilder.build()); + signalOnBusyStatusChanged(false); + stopForeground(true); + stopSelf(); + } + + @Override + public void onFailed(final Context context, final String errorMsg) + { + super.onFailed(context, errorMsg); + + Intent errorIntent = new Intent(context, SuntimesCalendarErrorActivity.class); + errorIntent.putExtra(SuntimesCalendarErrorActivity.EXTRA_ERROR_MESSAGE, errorMsg); + errorIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + context.startActivity(errorIntent); + + signalOnBusyStatusChanged(false); + stopForeground(true); + stopSelf(); + } + + private PendingIntent getNotificationIntent() + { + Intent intent = new Intent(Intent.ACTION_VIEW); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + { + Uri.Builder uriBuilder = CalendarContract.CONTENT_URI.buildUpon(); + uriBuilder.appendPath("time"); + ContentUris.appendId(uriBuilder, System.currentTimeMillis()); + intent = intent.setData(uriBuilder.build()); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + } + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return PendingIntent.getActivity(context, 0, intent, 0); + } + }; + calendarTask.setTaskListener(calendarTaskListener); + + if (clearCalendars) { + calendarTask.setFlagClearCalendars(true); + } + calendarTask.setItems(items.toArray(new SuntimesCalendarTask.SuntimesCalendarTaskItem[0])); + calendarTask.execute(); + return true; + } + + public boolean isBusy() + { + if (calendarTask != null) + { + switch (calendarTask.getStatus()) + { + case PENDING: + case RUNNING: + return true; + + case FINISHED: + default: + return false; + } + } else return false; + } + + private String lastProgressMessage; + public String getLastProgressMessage() + { + return lastProgressMessage; + } + + public static ArrayList loadItems(Intent intent, boolean clearPending) + { + SuntimesCalendarTask.SuntimesCalendarTaskItem[] items; + Parcelable[] parcelableArray = intent.getParcelableArrayExtra(EXTRA_CALENDAR_ITEMS); + if (parcelableArray != null) { + items = Arrays.copyOf(parcelableArray, parcelableArray.length, SuntimesCalendarTask.SuntimesCalendarTaskItem[].class); + } else items = new SuntimesCalendarTask.SuntimesCalendarTaskItem[0]; + + if (clearPending) { + intent.removeExtra(EXTRA_CALENDAR_ITEMS); + } + return new ArrayList<>(Arrays.asList(items)); + } + + /** + * SuntimesCalendarServiceListener + */ + public static abstract class SuntimesCalendarServiceListener implements Parcelable + { + public void onStartCommand(boolean result) {} + public void onBusyStatusChanged(boolean isBusy) {} + public void onProgressMessage(String message) {} + + public SuntimesCalendarServiceListener() {} + protected SuntimesCalendarServiceListener(Parcel in) {} + + @Override + public void writeToParcel(Parcel dest, int flags) {} + + @Override + public int describeContents() { + return 0; + } + } + + private ArrayList serviceListeners = new ArrayList<>(); + public void addCalendarServiceListener(SuntimesCalendarServiceListener listener) + { + serviceListeners.add(listener); + } + public void removeCalendarServiceListener(SuntimesCalendarServiceListener listener) + { + if (serviceListeners.contains(listener)) { + serviceListeners.remove(listener); + } + } + + private void signalOnStartCommand(boolean result) + { + for (SuntimesCalendarServiceListener listener : serviceListeners) { + if (listener != null) { + listener.onStartCommand(result); + } + } + } + + private void signalOnBusyStatusChanged(boolean isBusy) + { + for (SuntimesCalendarServiceListener listener : serviceListeners) { + if (listener != null) { + listener.onBusyStatusChanged(isBusy); + } + } + } + + private void signalOnProgressMessage(String message) + { + lastProgressMessage = message; + for (SuntimesCalendarServiceListener listener : serviceListeners) { + if (listener != null) { + listener.onProgressMessage(message); + } + } + } + +} diff --git a/app/src/main/res/drawable-hdpi/ic_action_update.png b/app/src/main/res/drawable-hdpi/ic_action_update.png new file mode 100644 index 00000000..e2a22fa1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_update.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_update_light.png b/app/src/main/res/drawable-hdpi/ic_action_update_light.png new file mode 100644 index 00000000..df87e5ba Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_update_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_update.png b/app/src/main/res/drawable-mdpi/ic_action_update.png new file mode 100644 index 00000000..c8c43e3d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_update.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_update_light.png b/app/src/main/res/drawable-mdpi/ic_action_update_light.png new file mode 100644 index 00000000..84b71db9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_update_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_update.png b/app/src/main/res/drawable-xhdpi/ic_action_update.png new file mode 100644 index 00000000..8ae786b2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_update.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_update_light.png b/app/src/main/res/drawable-xhdpi/ic_action_update_light.png new file mode 100644 index 00000000..ca84d91a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_update_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_update.png b/app/src/main/res/drawable-xxhdpi/ic_action_update.png new file mode 100644 index 00000000..9a8e3795 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_update.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_update_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_update_light.png new file mode 100644 index 00000000..84987892 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_update_light.png differ diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 53fd8421..a8e40627 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -6,6 +6,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4d524895..b3206a67 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -108,4 +108,6 @@ false 31536000000 63072000000 + true + true \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b78fc088..974f0783 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -9,6 +9,7 @@ @color/text_accent_light @color/text_disabled_light @drawable/ic_action_calendar_light + @drawable/ic_action_update_light @drawable/ic_action_about_light @drawable/ic_action_settings_light @drawable/ic_action_help_light @@ -21,8 +22,18 @@ @color/text_accent_dark @color/text_disabled_dark @drawable/ic_action_calendar + @drawable/ic_action_update @drawable/ic_action_about @drawable/ic_action_settings @drawable/ic_action_help + + + diff --git a/app/src/main/res/xml/preference_calendars.xml b/app/src/main/res/xml/preference_calendars.xml index 98c529cf..16194605 100644 --- a/app/src/main/res/xml/preference_calendars.xml +++ b/app/src/main/res/xml/preference_calendars.xml @@ -9,6 +9,18 @@ android:title="@string/configLabel_calendars_enabled" android:summary="@string/configLabel_calendars_enabled_summary" android:defaultValue="@string/def_app_calendars_enabled" /> + + + + + + + + + + + \ No newline at end of file