Add DavService for long-running operations

pull/2/head
Ricki Hirner 9 years ago
parent 77c947da14
commit fc29988dc6

@ -49,6 +49,7 @@
android:label="@string/app_name"
android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute">
<service
android:name=".syncadapter.AccountAuthenticatorService"
android:exported="false">
@ -103,12 +104,18 @@
android:resource="@xml/sync_tasks"/>
</service>
<service
android:name=".DavService"
android:enabled="true">
</service>
<activity
android:name=".ui.AccountsActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
@ -134,6 +141,7 @@
android:label="@string/settings_title">
<intent-filter>
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
@ -148,6 +156,7 @@
<activity
android:name="de.duenndns.ssl.MemorizingActivity"
android:theme="@android:style/Theme.Holo.Light.Dialog.NoActionBar"/>
</application>
</manifest>

@ -0,0 +1,109 @@
/*
* Copyright © 2013 2016 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public class DavService extends Service {
public static final String
ACTION_REFRESH_COLLECTIONS = "refreshCollections",
EXTRA_DAV_SERVICE_ID = "davServiceID";
private final IBinder binder = new InfoBinder();
private final Set<Long> runningRefresh = new HashSet<>();
private final List<RefreshingStatusListener> refreshingStatusListeners = new LinkedList<>();
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
long id = intent.getLongExtra(EXTRA_DAV_SERVICE_ID, -1);
switch (action) {
case ACTION_REFRESH_COLLECTIONS:
if (runningRefresh.add(id)) {
new Thread(new RefreshCollections(id)).start();
for (RefreshingStatusListener listener : refreshingStatusListeners)
listener.onDavRefreshStatusChanged(id, true);
}
break;
}
return START_NOT_STICKY;
}
/* BOUND SERVICE PART
for communicating with the activities
*/
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public interface RefreshingStatusListener {
void onDavRefreshStatusChanged(long id, boolean refreshing);
}
public class InfoBinder extends Binder {
public boolean isRefreshing(long id) {
return runningRefresh.contains(id);
}
public void addRefreshingStatusListener(RefreshingStatusListener listener, boolean callImmediate) {
refreshingStatusListeners.add(listener);
if (callImmediate)
for (long id : runningRefresh)
listener.onDavRefreshStatusChanged(id, true);
}
public void removeRefreshingStatusListener(RefreshingStatusListener listener) {
refreshingStatusListeners.remove(listener);
}
}
/* ACTION RUNNABLES
which actually do the work
*/
private class RefreshCollections implements Runnable {
final long serviceId;
RefreshCollections(long davServiceId) {
this.serviceId = davServiceId;
}
@Override
public void run() {
Constants.log.debug("RefreshCollections.Runner STARTING {}", serviceId);
try {
Thread.currentThread().sleep(10000);
} catch (InterruptedException e) {
} finally {
Constants.log.debug("RefreshCollections.Runner FINISHED {}", serviceId);
runningRefresh.remove(serviceId);
for (RefreshingStatusListener listener : refreshingStatusListeners)
listener.onDavRefreshStatusChanged(serviceId, false);
}
}
}
}

@ -16,27 +16,44 @@ import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.LoaderManager;
import android.content.AsyncTaskLoader;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.Loader;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.os.IBinder;
import android.support.v4.app.DialogFragment;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.CardView;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ProgressBar;
import java.io.IOException;
import java.util.List;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.DavService;
import at.bitfire.davdroid.syncadapter.ServiceDB.*;
import lombok.Cleanup;
public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenuItemClickListener {
public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenuItemClickListener, ServiceConnection, DavService.RefreshingStatusListener, LoaderManager.LoaderCallbacks<AccountActivity.AccountInfo> {
public static final String EXTRA_ACCOUNT_NAME = "account_name";
Account account;
private String accountName;
private AccountInfo accountInfo;
private DavService.InfoBinder davService;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -46,7 +63,6 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
if (accountName == null)
// invalid account name
finish();
account = new Account(accountName, Constants.ACCOUNT_TYPE);
setTitle(accountName);
setContentView(R.layout.activity_account);
@ -54,6 +70,24 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
Toolbar toolbar = (Toolbar)findViewById(R.id.carddav_menu);
toolbar.inflateMenu(R.menu.carddav_actions);
toolbar.setOnMenuItemClickListener(this);
toolbar = (Toolbar)findViewById(R.id.caldav_menu);
toolbar.inflateMenu(R.menu.caldav_actions);
toolbar.setOnMenuItemClickListener(this);
getLoaderManager().initLoader(0, getIntent().getExtras(), this);
}
@Override
protected void onResume() {
super.onResume();
bindService(new Intent(this, DavService.class), this, Context.BIND_AUTO_CREATE);
}
@Override
protected void onPause() {
super.onPause();
unbindService(this);
}
@Override
@ -66,24 +100,18 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.delete_account:
DialogFragment frag = new DialogFragment() {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(AccountActivity.this)
.setIcon(R.drawable.ic_error_dark)
.setTitle(R.string.account_delete_confirmation_title)
.setMessage(R.string.account_delete_confirmation_text)
.setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
deleteAccount();
}
})
.create();
}
};
frag.show(getSupportFragmentManager(), null);
new AlertDialog.Builder(AccountActivity.this)
.setIcon(R.drawable.ic_error_dark)
.setTitle(R.string.account_delete_confirmation_title)
.setMessage(R.string.account_delete_confirmation_text)
.setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
deleteAccount();
}
})
.show();
break;
default:
return super.onOptionsItemSelected(item);
@ -93,11 +121,155 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.refresh_address_books:
Intent intent = new Intent(this, DavService.class);
intent.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
intent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, accountInfo.cardDavService);
startService(intent);
break;
case R.id.refresh_calendars:
intent = new Intent(this, DavService.class);
intent.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
intent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, accountInfo.calDavService);
startService(intent);
break;
}
return false;
}
/* SERVICE CONNECTION */
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
davService = (DavService.InfoBinder)service;
davService.addRefreshingStatusListener(this, true);
}
@Override
public void onServiceDisconnected(ComponentName name) {
davService.removeRefreshingStatusListener(this);
davService = null;
}
@Override
public void onDavRefreshStatusChanged(long id, boolean refreshing) {
getLoaderManager().restartLoader(0, getIntent().getExtras(), this);
}
/* LOADERS AND LOADED DATA */
public static class AccountInfo {
Long cardDavService;
boolean cardDavRefreshing;
Long calDavService;
boolean calDavRefreshing;
}
@Override
public Loader<AccountInfo> onCreateLoader(int id, Bundle args) {
return new AccountLoader(this, args.getString(EXTRA_ACCOUNT_NAME));
}
@Override
public void onLoadFinished(Loader<AccountInfo> loader, AccountInfo info) {
accountInfo = info;
CardView card = (CardView)findViewById(R.id.carddav);
if (info.cardDavService != null) {
ProgressBar progress = (ProgressBar)findViewById(R.id.carddav_refreshing);
progress.setVisibility(info.cardDavRefreshing ? View.VISIBLE : View.GONE);
} else
card.setVisibility(View.GONE);
card = (CardView)findViewById(R.id.caldav);
if (info.calDavService != null) {
ProgressBar progress = (ProgressBar)findViewById(R.id.caldav_refreshing);
progress.setVisibility(info.calDavRefreshing ? View.VISIBLE : View.GONE);
} else
card.setVisibility(View.GONE);
}
@Override
public void onLoaderReset(Loader<AccountInfo> loader) {
}
private static class AccountLoader extends AsyncTaskLoader<AccountInfo> implements DavService.RefreshingStatusListener, ServiceConnection {
private final String accountName;
private final OpenHelper dbHelper;
private DavService.InfoBinder davService;
public AccountLoader(Context context, String accountName) {
super(context);
this.accountName = accountName;
dbHelper = new OpenHelper(context);
}
@Override
protected void onStartLoading() {
getContext().bindService(new Intent(getContext(), DavService.class), this, Context.BIND_AUTO_CREATE);
}
@Override
protected void onStopLoading() {
davService.removeRefreshingStatusListener(this);
getContext().unbindService(this);
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
davService = (DavService.InfoBinder)service;
forceLoad();
}
@Override
public void onServiceDisconnected(ComponentName name) {
davService = null;
}
@Override
public void onDavRefreshStatusChanged(long id, boolean refreshing) {
forceLoad();
}
@Override
public AccountInfo loadInBackground() {
AccountInfo info = new AccountInfo();
try {
SQLiteDatabase db = dbHelper.getReadableDatabase();
@Cleanup Cursor cursor = db.query(
Services._TABLE,
new String[] { Services.ID, Services.SERVICE },
Services.ACCOUNT_NAME + "=?", new String[] { accountName },
null, null, null);
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
String service = cursor.getString(1);
if (Services.SERVICE_CARDDAV.equals(service)) {
info.cardDavService = id;
info.cardDavRefreshing = davService.isRefreshing(id);
} else if (Services.SERVICE_CALDAV.equals(service)) {
info.calDavService = id;
info.calDavRefreshing = davService.isRefreshing(id);
}
}
} finally {
dbHelper.close();
}
return info;
}
}
/* USER ACTIONS */
protected void deleteAccount() {
Account account = new Account(accountName, Constants.ACCOUNT_TYPE);
AccountManager accountManager = AccountManager.get(this);
if (Build.VERSION.SDK_INT >= 22)

@ -11,11 +11,14 @@ package at.bitfire.davdroid.ui;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.OnAccountsUpdateListener;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
@ -23,25 +26,30 @@ import android.support.v4.content.Loader;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.LinkedList;
import java.util.List;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.DavService;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.syncadapter.ServiceDB;
import at.bitfire.davdroid.syncadapter.ServiceDB.*;
import at.bitfire.davdroid.syncadapter.ServiceDB.OpenHelper;
import at.bitfire.davdroid.syncadapter.ServiceDB.Services;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
public class AccountListFragment extends ListFragment implements OnAccountsUpdateListener, LoaderManager.LoaderCallbacks<List<AccountListFragment.AccountInfo>>, AdapterView.OnItemClickListener {
protected AccountManager accountManager;
private AccountManager accountManager;
private DavService.InfoBinder davService;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -85,6 +93,7 @@ public class AccountListFragment extends ListFragment implements OnAccountsUpdat
getLoaderManager().restartLoader(0, null, this);
}
@Override
public Loader<List<AccountInfo>> onCreateLoader(int id, Bundle args) {
return new AccountLoader(getContext());
@ -107,7 +116,8 @@ public class AccountListFragment extends ListFragment implements OnAccountsUpdat
@RequiredArgsConstructor
public static class AccountInfo {
final Account account;
boolean hasCardDAV, hasCalDAV;
boolean isRefreshing;
Long cardDavService, calDavService;
}
@ -131,20 +141,29 @@ public class AccountListFragment extends ListFragment implements OnAccountsUpdat
TextView tv = (TextView)v.findViewById(R.id.account_name);
tv.setText(info.account.name);
ProgressBar progressRefresh = (ProgressBar)v.findViewById(R.id.refreshing);
progressRefresh.setVisibility(info.isRefreshing ? View.VISIBLE : View.GONE);
Animation blink = new AlphaAnimation(0, 1);
blink.setDuration(400);
blink.setRepeatMode(Animation.REVERSE);
blink.setRepeatCount(Animation.INFINITE);
tv = (TextView)v.findViewById(R.id.carddav);
tv.setVisibility(info.hasCardDAV ? View.VISIBLE : View.GONE);
tv.setVisibility(info.cardDavService != null ? View.VISIBLE : View.GONE);
tv.setAnimation(info.isRefreshing ? blink : null);
tv = (TextView)v.findViewById(R.id.caldav);
tv.setVisibility(info.hasCalDAV ? View.VISIBLE : View.GONE);
tv.setVisibility(info.calDavService != null ? View.VISIBLE : View.GONE);
return v;
}
}
static class AccountLoader extends AsyncTaskLoader<List<AccountInfo>> {
final AccountManager accountManager;
final OpenHelper dbHelper;
private static class AccountLoader extends AsyncTaskLoader<List<AccountInfo>> {
private final AccountManager accountManager;
private final OpenHelper dbHelper;
public AccountLoader(Context context) {
super(context);
@ -159,6 +178,7 @@ public class AccountListFragment extends ListFragment implements OnAccountsUpdat
@Override
public List<AccountInfo> loadInBackground() {
Constants.log.info("AccountLoader RUNNING");
List<AccountInfo> accounts = new LinkedList<>();
try {
SQLiteDatabase db = dbHelper.getReadableDatabase();
@ -169,15 +189,17 @@ public class AccountListFragment extends ListFragment implements OnAccountsUpdat
// query services of this account
@Cleanup Cursor cursor = db.query(
Services._TABLE,
new String[] { Services.SERVICE },
new String[] { Services.ID, Services.SERVICE },
Services.ACCOUNT_NAME + "=?", new String[] { account.name },
null, null, null);
while (cursor.moveToNext()) {
String service = cursor.getString(0);
long id = cursor.getLong(0);
String service = cursor.getString(1);
if (Services.SERVICE_CARDDAV.equals(service))
info.hasCardDAV = true;
info.cardDavService = id;
if (Services.SERVICE_CALDAV.equals(service))
info.hasCalDAV = true;
info.calDavService = id;
}
accounts.add(info);
@ -187,6 +209,7 @@ public class AccountListFragment extends ListFragment implements OnAccountsUpdat
}
return accounts;
}
}
}

@ -12,8 +12,8 @@
android:layout_height="match_parent">
<ImageView
android:src="@drawable/blue_sky_with_birds"
android:scaleType="fitCenter"
android:src="@drawable/sky_birds"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true" />

@ -28,6 +28,7 @@
android:gravity="right|center_vertical">
<ProgressBar
android:id="@+id/refreshing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"

@ -18,19 +18,67 @@
android:layout_marginBottom="@dimen/activity_vertical_margin">
<android.support.v7.widget.CardView
android:id="@+id/carddav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="8dp"
>
android:layout_marginBottom="16dp"
app:cardElevation="8dp">
<android.support.v7.widget.Toolbar
android:id="@+id/carddav_menu"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:theme="@style/toolbar_theme"
style="@style/toolbar_style"
app:navigationIcon="@drawable/ic_people_light"
app:title="CardDAV"/>
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/carddav_menu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/toolbar_theme"
style="@style/toolbar_style"
app:navigationIcon="@drawable/ic_people_light"
app:title="CardDAV"/>
<ProgressBar
android:id="@+id/carddav_refreshing"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:indeterminate="true"/>
</LinearLayout>
</android.support.v7.widget.CardView>
<android.support.v7.widget.CardView
android:id="@+id/caldav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/caldav_menu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/toolbar_theme"
style="@style/toolbar_style"
app:navigationIcon="@drawable/ic_people_light"
app:title="CalDAV"/>
<ProgressBar
android:id="@+id/caldav_refreshing"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:indeterminate="true"/>
</LinearLayout>
</android.support.v7.widget.CardView>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2013 2016 Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/refresh_calendars"
android:title="@string/account_refresh_calendar_list"/>
<item android:id="@+id/add_calendar"
android:title="@string/account_add_existing_calendar"/>
<item android:id="@+id/create_calendar"
android:title="@string/account_create_new_calendar"/>
</menu>

@ -9,12 +9,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/refresh"
<item android:id="@+id/refresh_address_books"
android:title="@string/account_refresh_address_book_list"/>
<item android:id="@+id/add"
<item android:id="@+id/add_address_book"
android:title="@string/account_add_existing_address_book"/>
<item android:id="@+id/create"
<item android:id="@+id/create_address_book"
android:title="@string/account_create_new_address_book"/>
</menu>

@ -34,6 +34,9 @@
<string name="account_refresh_address_book_list">Refresh address book list</string>
<string name="account_add_existing_address_book">Add existing address book</string>
<string name="account_create_new_address_book">Create new address book</string>
<string name="account_refresh_calendar_list">Refresh calendar list</string>
<string name="account_add_existing_calendar">Add existing calendar</string>
<string name="account_create_new_calendar">Create new calendar</string>
<!-- MainActivity -->
<string name="main_manage_accounts">Manage sync accounts</string>

@ -76,7 +76,7 @@
<item name="android:textColorSecondary">@color/white</item>
</style>
<style name="toolbar_style" parent="Widget.AppCompat.Toolbar">
<item name="android:background">@color/davdroid_green</item>
<item name="android:background">@color/davdroid_lightblue</item>
<item name="titleTextColor">@color/white</item>
</style>

Loading…
Cancel
Save