diff --git a/CREDITS.markdown b/CREDITS.markdown index cd961562..cd6e645b 100644 --- a/CREDITS.markdown +++ b/CREDITS.markdown @@ -43,6 +43,8 @@ the alternative [Git logo](http://henrik.nyh.se/2007/06/alternative-git-logo-and * Bernd Hirschler - [German Translation](https://github.com/rtyley/agit/pull/35) +* Scott Johnson - [Sync Frequency UI](https://github.com/rtyley/agit/pull/103) + * Eddie Ringle - [Tweak GitHub url parsing](https://github.com/rtyley/agit/pull/29) * Leonardo Taglialegne - [Italian Translation](https://github.com/rtyley/agit/pull/46) diff --git a/agit/AndroidManifest.xml b/agit/AndroidManifest.xml index fcef7206..f09f1c72 100644 --- a/agit/AndroidManifest.xml +++ b/agit/AndroidManifest.xml @@ -15,6 +15,12 @@ + + + + + + @@ -142,6 +148,12 @@ + + + + + + @@ -184,4 +196,5 @@ + \ No newline at end of file diff --git a/agit/res/layout/settings_activity.xml b/agit/res/layout/settings_activity.xml new file mode 100644 index 00000000..eddecee5 --- /dev/null +++ b/agit/res/layout/settings_activity.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/agit/res/menu/dashboard.xml b/agit/res/menu/dashboard.xml index a79d1556..37617f69 100644 --- a/agit/res/menu/dashboard.xml +++ b/agit/res/menu/dashboard.xml @@ -10,6 +10,11 @@ android:title="@string/open_menu_option" android:showAsAction="never"> + + SSH Install Guide Clone... Clone... + Settings... Url Target Folder Repository url @@ -112,4 +113,46 @@ "No viewer available for '%1$s'" + + Settings + Sync Settings + Sync Frequency + Tap to set how often repository synchronization will occur + setting_sync_frequency + + 0 + Sync daily at + setting_sync_frequency_daily_hour + setting_sync_frequency_daily_min + setting_sync_frequency_subtitle + + + + Manually + Schedule daily at… + Every minute + Every 5 minutes + Every 15 minutes + Every hour + Every 4 hours + Every day + Every week + + + + + -1 + @string/setting_sync_frequency_daily + 1 + 5 + 15 + 60 + 240 + 1440 + 10080 + diff --git a/agit/src/main/java/com/madgag/agit/DashboardActivity.java b/agit/src/main/java/com/madgag/agit/DashboardActivity.java index 014a0a20..5f992b89 100644 --- a/agit/src/main/java/com/madgag/agit/DashboardActivity.java +++ b/agit/src/main/java/com/madgag/agit/DashboardActivity.java @@ -43,7 +43,6 @@ import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuItem; import com.github.rtyley.android.sherlock.roboguice.activity.RoboSherlockFragmentActivity; -import com.madgag.android.IntentUtil; import com.madgag.android.util.store.InstallAppDialogFragment; import java.io.File; @@ -81,6 +80,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.clone: startActivity(new Intent(this, CloneLauncherActivity.class)); return true; + case R.id.settings: + startActivity(new Intent(this, SettingsActivity.class)); + return true; case R.id.open_repo: if (isIntentAvailable(this, PICK_DIRECTORY_INTENT)) { Intent intent = new Intent(PICK_DIRECTORY_INTENT); diff --git a/agit/src/main/java/com/madgag/agit/SettingsActivity.java b/agit/src/main/java/com/madgag/agit/SettingsActivity.java new file mode 100644 index 00000000..d9fdbbe8 --- /dev/null +++ b/agit/src/main/java/com/madgag/agit/SettingsActivity.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2011, 2012 Roberto Tyley + * + * This file is part of 'Agit' - an Android Git client. + * + * Agit 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. + * + * Agit 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 this program. If not, see http://www.gnu.org/licenses/ . + */ + +package com.madgag.agit; + +import static com.madgag.agit.sync.AccountAuthenticatorService.addAccount; +import android.app.TimePickerDialog; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceManager; +import android.util.Log; +import android.widget.TimePicker; + +import com.madgag.agit.sync.SyncRepoManager; + +public class SettingsActivity extends PreferenceActivity { + public void onCreate(Bundle aSavedInstanceState) { + super.onCreate(aSavedInstanceState); + addPreferencesFromResource(R.layout.settings_activity); + ListPreference syncFreq = (ListPreference) findPreference(getString(R.string.setting_sync_frequency_key)); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsActivity.this); + setSummary(); + syncFreq.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object o) { + String value = (String)o; + final SharedPreferences.Editor edtr = prefs.edit(); + + if (value.equals(getString(R.string.setting_sync_frequency_daily))) { + // Check if our previous value was the same thing, so we can set it in the time + // picker dialog. + int hourDefault = 12; + int minDefault = 0; + String prevVal = prefs.getString(getString(R.string.setting_sync_frequency_key), "-1"); + if (prevVal.equals(o)) { + hourDefault = prefs.getInt(getString(R.string.setting_sync_frequency_daily_hour_key), hourDefault); + minDefault = prefs.getInt(getString(R.string.setting_sync_frequency_daily_min_key), minDefault); + } + + TimePickerDialog.OnTimeSetListener timeListener = new TimePickerDialog.OnTimeSetListener() { + @Override + public void onTimeSet(TimePicker timePicker, int hourOfDay, int minOfHour) { + String subTitle = "Sync daily at " + hourOfDay + ":" + (minOfHour < 10 ? "0" : "") + minOfHour; + edtr.putInt(getString(R.string.setting_sync_frequency_daily_hour_key), hourOfDay); + edtr.putInt(getString(R.string.setting_sync_frequency_daily_min_key), minOfHour); + edtr.putString(getString(R.string.setting_sync_frequency_subtitle_key), subTitle); + edtr.commit(); + + SyncRepoManager manager = new SyncRepoManager(); + manager.setDailySync(SettingsActivity.this, hourOfDay, minOfHour); + SettingsActivity.this.setSummary(); + } + }; + + // Display new dialog with the available options. + TimePickerDialog timePicker = new TimePickerDialog(SettingsActivity.this, timeListener, hourDefault, minDefault, false); + timePicker.setTitle(getString(R.string.setting_sync_frequency_daily_title)); + timePicker.show(); + } else { + // Loop to find out what the index of the chosen value is in the array - it should be the same + // as the chosen value in the array of possible choices (see comment in strings.xml). + String[] indexArray = getResources().getStringArray(R.array.setting_sync_choices_values); + String[] subtitleArray = getResources().getStringArray(R.array.setting_sync_choices); + int subtitleIndex = 0; + for (String s : indexArray) { + if (s.equals(o)) { + break; + } + subtitleIndex++; + } + + String subTitle = subtitleArray[subtitleIndex]; + edtr.putString(getString(R.string.setting_sync_frequency_subtitle_key), subTitle); + edtr.commit(); + SyncRepoManager manager = new SyncRepoManager(); + manager.cancelDailySync(SettingsActivity.this); + } + + SettingsActivity.this.setSummary(); + + return true; + } + }); + } + + @Override + public void onDestroy() { + super.onDestroy(); + try { + addAccount(this); + } catch (Exception e) { + Log.w(TAG, "Unable to re-add account for syncing after preference changes", e); + } + } + + /** + * Displays the value stored in the preference setting_sync_frequency_subtitle_key in the summary line of the + * list preference, so the user doesn't have to click through to view which option is selected. + */ + private void setSummary() { + ListPreference syncFreq = (ListPreference) findPreference(getString(R.string.setting_sync_frequency_key)); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsActivity.this); + syncFreq.setSummary(prefs.getString(getString(R.string.setting_sync_frequency_subtitle_key), + getString(R.string.setting_instruction_sync_frequency))); + } + + private static final String TAG = "SettingsActivity"; +} diff --git a/agit/src/main/java/com/madgag/agit/sync/AccountAuthenticatorService.java b/agit/src/main/java/com/madgag/agit/sync/AccountAuthenticatorService.java index c25f7c6e..143ac443 100644 --- a/agit/src/main/java/com/madgag/agit/sync/AccountAuthenticatorService.java +++ b/agit/src/main/java/com/madgag/agit/sync/AccountAuthenticatorService.java @@ -34,8 +34,10 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.os.IBinder; +import android.preference.PreferenceManager; import android.util.Log; /** @@ -71,15 +73,28 @@ public static Bundle addAccount(Context ctx) { result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); } - configureSyncFor(account); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); + int syncFreq = Integer.parseInt(prefs.getString("setting_sync_frequency", "-1")); + configureSyncFor(account, syncFreq); + return result; } - private static void configureSyncFor(Account account) { - Log.d(TAG, "Trying to configure account for sync..."); - setIsSyncable(account, AGIT_PROVIDER_AUTHORITY, 1); - setSyncAutomatically(account, AGIT_PROVIDER_AUTHORITY, true); - ContentResolver.addPeriodicSync(account, AGIT_PROVIDER_AUTHORITY, new Bundle(), (long) (15 * 60)); + private static void configureSyncFor(Account aAccount, int aSyncFreq) { + // There are three possible sync settings we could have: + // < 0: Indicates sync is disabled + // == 0: Indicates sync is enabled, but it's handled by an alarm because it's a time-of-day sync + // > 0: Indicates sync should be enabled for aSyncFreq minutes + if (aSyncFreq < 0) { + Log.d(TAG, "Disabling sync settings"); + setIsSyncable(aAccount, AGIT_PROVIDER_AUTHORITY, 0); + ContentResolver.removePeriodicSync(aAccount, AGIT_PROVIDER_AUTHORITY, new Bundle()); + } else { + Log.d(TAG, "Trying to configure account for sync at rate of " + aSyncFreq + " minutes"); + setIsSyncable(aAccount, AGIT_PROVIDER_AUTHORITY, 1); + setSyncAutomatically(aAccount, AGIT_PROVIDER_AUTHORITY, true); + ContentResolver.addPeriodicSync(aAccount, AGIT_PROVIDER_AUTHORITY, new Bundle(), (long) (aSyncFreq * 60)); + } } private static class AccountAuthenticatorImpl extends AbstractAccountAuthenticator { diff --git a/agit/src/main/java/com/madgag/agit/sync/SyncAdapter.java b/agit/src/main/java/com/madgag/agit/sync/SyncAdapter.java index b74251e9..5ec2de74 100644 --- a/agit/src/main/java/com/madgag/agit/sync/SyncAdapter.java +++ b/agit/src/main/java/com/madgag/agit/sync/SyncAdapter.java @@ -25,6 +25,7 @@ import android.content.Context; import android.content.SyncResult; import android.os.Bundle; +import android.util.Log; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -50,6 +51,7 @@ public SyncAdapter(Context context) { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { + Log.d(TAG, "Performing sync"); cancelAnyCurrentCampaign(); contextScope.enter(getContext()); try { diff --git a/agit/src/main/java/com/madgag/agit/sync/SyncRepoManager.java b/agit/src/main/java/com/madgag/agit/sync/SyncRepoManager.java new file mode 100644 index 00000000..90431be0 --- /dev/null +++ b/agit/src/main/java/com/madgag/agit/sync/SyncRepoManager.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2011, 2012 Roberto Tyley + * + * This file is part of 'Agit' - an Android Git client. + * + * Agit 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. + * + * Agit 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 this program. If not, see http://www.gnu.org/licenses/ . + */ + +package com.madgag.agit.sync; + +import static com.madgag.agit.sync.Constants.AGIT_ACCOUNT_NAME; +import static com.madgag.agit.sync.Constants.AGIT_ACCOUNT_TYPE; +import static com.madgag.agit.sync.Constants.AGIT_PROVIDER_AUTHORITY; +import android.accounts.Account; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import java.util.Calendar; +import java.util.TimeZone; + +/** + * Manager class for daily synchronization settings. Because periodic sync at time-of-day intervals is handled + * differently than periodic sync on a regular schedule, we use this class as a receiver of alerts on a regular + * interval from AlarmManager. + */ +public class SyncRepoManager extends BroadcastReceiver { + + public SyncRepoManager() { + + } + + /** + * Setup a sync at a specific hour and minute of the day. Every effort is made to make the sync happen at the time + * of day specified, but it's not guaranteed. Because we use inexact repeating alarms, it will never happen before + * the specified time, but it may happen quite a while afterwards. This is because the Android operating system + * tries to phase alarms so they don't happen at the same time. + * + * @param aContext The context from which the daily sync is being set up. + * @param aHour The hour of the day (from 0 to 24) at which the sync should happen + * @param aMin The minute of the hour at which the sync should happen. + */ + public static void setDailySync(Context aContext, int aHour, int aMin) { + AlarmManager mgr =(AlarmManager)aContext.getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent("com.madgag.agit.sync.SET_DAILY_SYNC"); + PendingIntent pend = PendingIntent.getBroadcast(aContext, 0, intent, 0); + + Calendar updateTime = Calendar.getInstance(); + updateTime.setTimeZone(TimeZone.getDefault()); + updateTime.set(Calendar.HOUR_OF_DAY, aHour); + updateTime.set(Calendar.MINUTE, aMin); + long msTimeToRepeat = updateTime.getTimeInMillis(); + + mgr.setInexactRepeating(AlarmManager.RTC_WAKEUP, msTimeToRepeat, AlarmManager.INTERVAL_DAY, pend); + } + + /** + * Cancel a daily sync already set up. + * + * @param aContext The context from which the daily sync cancellation is being requested. + */ + public static void cancelDailySync(Context aContext) { + Log.d(TAG, "Canceling daily sync"); + AlarmManager mgr =(AlarmManager)aContext.getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent("com.madgag.agit.sync.SET_DAILY_SYNC"); + PendingIntent pend = PendingIntent.getBroadcast(aContext, 0, intent, 0); + mgr.cancel(pend); + } + + @Override + public void onReceive(Context aContext, Intent aIntent) { + Calendar rightNow = Calendar.getInstance(); + long offset = rightNow.get(Calendar.ZONE_OFFSET) + + rightNow.get(Calendar.DST_OFFSET); + long sinceMidnight = (rightNow.getTimeInMillis() + offset) % + (24 * 60 * 60 * 1000); + int hoursSinceMidnight = (int)(sinceMidnight / (60*60*1000)); + boolean isPM = hoursSinceMidnight > 12; + int clockHour = hoursSinceMidnight % 12; + int minutesSinceMidnight = (int)((sinceMidnight % (hoursSinceMidnight * (60 * 60 * 1000))) / (60*1000)); + + Log.d(TAG, "onReceive called at " + clockHour + ":" + minutesSinceMidnight + " " + (isPM ? "pm" : "am")); + doSyncNow(); + } + + /** + * Triggers a manual synchronization of the repositories. This the main event that is performed + * after onReceive() is seen. + */ + private void doSyncNow() { + Bundle settingsBundle = new Bundle(); + settingsBundle.putBoolean( + ContentResolver.SYNC_EXTRAS_MANUAL, true); + settingsBundle.putBoolean( + ContentResolver.SYNC_EXTRAS_EXPEDITED, true); + + Account account = new Account(AGIT_ACCOUNT_NAME, AGIT_ACCOUNT_TYPE); + ContentResolver.requestSync(account, AGIT_PROVIDER_AUTHORITY, settingsBundle); + } + + private static final String TAG = "SyncRepoManager"; +}