Skip to content

Commit

Permalink
Refactor code that depends on a logged in user
Browse files Browse the repository at this point in the history
Makes use of dagger subcomponents to have seperate state for each
logged in user. This is useful for when we want to allow to have more
than one account in the app. But already now it resolves a lot of bugs
in case an account is removed at any time. And it improves the
readability of the code as dependencies are much more explicit.
Moreover, it makes the story of "what is available when?" much easier to
understand.
  • Loading branch information
saemy committed Jun 1, 2018
1 parent 993cef9 commit 38b7eb1
Show file tree
Hide file tree
Showing 67 changed files with 1,580 additions and 941 deletions.
13 changes: 7 additions & 6 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
Expand Down Expand Up @@ -43,13 +43,14 @@
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>

<service
android:name=".WSAndroidService"
android:exported="false">
</service>
android:name=".AutoMessageReloadJobService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".AutoMessageReloadService"
android:exported="false" />

<!-- Start the WS service at boot time -->
<receiver android:name=".BootCompletedIntentReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package fi.bitrite.android.ws;

import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.Log;

import javax.inject.Inject;

import io.reactivex.disposables.Disposable;

/**
* Runs the reloading of messages in a scheduled job service. That uses less battery power than
* using a traditional service which is running all the time in the background.
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class AutoMessageReloadJobService extends JobService {
private final static String TAG = AutoMessageReloadJobService.class.getCanonicalName();
private final static int JOB_ID_RELOAD_MESSAGES = 1;

@Inject AutoMessageReloadScheduler mAutoMessageReloadScheduler;

private Disposable mDisposable;

public static void reschedule(Context context, long messageReloadIntervalMs) {
JobScheduler jobScheduler =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
if (jobScheduler == null) {
return;
}

if (messageReloadIntervalMs > 0) {
ComponentName componentName =
new ComponentName(context, AutoMessageReloadJobService.class);
JobInfo jobInfo = new JobInfo.Builder(JOB_ID_RELOAD_MESSAGES, componentName)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setMinimumLatency(messageReloadIntervalMs)
.build();

int resultCode = jobScheduler.schedule(jobInfo);
if (resultCode != JobScheduler.RESULT_SUCCESS) {
Log.e(TAG, "Message reload job failed to be scheduled.");
}
} else {
jobScheduler.cancel(JOB_ID_RELOAD_MESSAGES);
}
}

@Override
public boolean onStartJob(JobParameters jobParameters) {
WSAndroidApplication.getAppComponent().inject(this);

Log.d(TAG, "Auto-reloading messages");

mDisposable = mAutoMessageReloadScheduler.reloadMessagesInAllAccounts()
.onErrorComplete()
.subscribe(() -> {
mDisposable = null;

Log.d(TAG, "Auto-reloading messages completed");

// Mark the job as finished and do NOT request a reschedule. A reschedule should
// take place in case of an error and has nothing to do with repeated job
// execution. It would apply back-off policy for the job.
jobFinished(jobParameters, false);

// Reschedule the job.
reschedule(getApplicationContext(),
mAutoMessageReloadScheduler.getMessageReloadIntervalMs());
});

return true;
}

@Override
public boolean onStopJob(JobParameters jobParameters) {
Log.d(TAG, "Auto-reload messages job stopped.");
if (mDisposable != null) {
mDisposable.dispose();
mDisposable = null;
}

// Try to reschedule. This is never done in case of the reload time being zero.
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package fi.bitrite.android.ws;

import android.accounts.Account;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;

import fi.bitrite.android.ws.auth.AccountManager;
import fi.bitrite.android.ws.di.AppScope;
import fi.bitrite.android.ws.di.account.AccountComponentManager;
import fi.bitrite.android.ws.repository.MessageRepository;
import fi.bitrite.android.ws.repository.SettingsRepository;
import io.reactivex.Completable;

@AppScope
public class AutoMessageReloadScheduler
implements SharedPreferences.OnSharedPreferenceChangeListener {
private final AccountComponentManager mAccountComponentManager;
private final AccountManager mAccountManager;
private final Context mContext;
private final SettingsRepository mSettingsRepository;

private boolean mHasAccounts = true;
private long mMessageReloadIntervalMs;

@Inject
AutoMessageReloadScheduler(AccountManager accountManager,
AccountComponentManager accountComponentManager, Context context,
SettingsRepository settingsRepository) {
mAccountComponentManager = accountComponentManager;
mAccountManager = accountManager;
mContext = context;
mSettingsRepository = settingsRepository;

mAccountManager.getAccounts().subscribe(accounts -> {
boolean hasAccounts = accounts.length > 0;
if (hasAccounts == mHasAccounts) {
return;
}
mHasAccounts = hasAccounts;

reschedule();
});

// Register for settings updates. That triggers an initial run of the change listener.
mSettingsRepository.registerOnChangeListener(this);
}

public long getMessageReloadIntervalMs() {
return mHasAccounts ? mMessageReloadIntervalMs : 0;
}

public static class AccountHelper {
@Inject MessageRepository messageRepository;
}

public Completable reloadMessagesInAllAccounts() {
AccountHelper accountHelper = new AccountHelper();
List<Completable> completables = new LinkedList<>();
for (Account account : mAccountManager.getAccounts().getValue()) {
mAccountComponentManager.get(account).inject(accountHelper);
completables.add(accountHelper.messageRepository.reloadThreads());
}
return Completable.mergeDelayError(completables);
}

@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key == null || key.equals(mSettingsRepository.getMessageRefreshIntervalKey())) {
mMessageReloadIntervalMs = TimeUnit.MINUTES.toMillis(
mSettingsRepository.getMessageRefreshIntervalMin());
reschedule();
}
}

private void reschedule() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AutoMessageReloadJobService.reschedule(mContext, getMessageReloadIntervalMs());
} else {
AutoMessageReloadService.reschedule(mContext, getMessageReloadIntervalMs());
}

}
}

118 changes: 118 additions & 0 deletions app/src/main/java/fi/bitrite/android/ws/AutoMessageReloadService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package fi.bitrite.android.ws;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

import java.util.Timer;
import java.util.TimerTask;

import javax.annotation.Nullable;
import javax.inject.Inject;

import io.reactivex.disposables.Disposable;

public class AutoMessageReloadService extends Service {
private final static String TAG = AutoMessageReloadService.class.getCanonicalName();

@Inject AutoMessageReloadScheduler mAutoMessageReloadScheduler;

private Timer mTimer;
private ReloadMessagesTask mReloadTask;

/**
* Starts the service to periodically reload the message threads.
*/
public static void reschedule(Context context, long messageReloadInterval) {
Intent serviceIntent = new Intent(context, AutoMessageReloadService.class);
if (messageReloadInterval > 0) {
context.startService(serviceIntent);
} else {
context.stopService(serviceIntent);
}
}

@Override
public void onCreate() {
Log.d(TAG, "Auto-message reload service: created.");
super.onCreate();

WSAndroidApplication.getAppComponent().inject(this);

// Reload the messages now.
mTimer = new Timer();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// This method is fired every time we do a startService call in reschedule().
Log.d(TAG, "Auto-message reload service: onStartCommand trigger.");
scheduleReloadTask();

return Service.START_STICKY;
}

@Override
public void onDestroy() {
Log.d(TAG, "Auto-message reload service: destroyed.");
mTimer.cancel();
super.onDestroy();
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}

void scheduleReloadTask() {
// Cancel any scheduled task.
if (mReloadTask != null) {
Log.d(TAG, "Cancelling currently scheduled task");
mReloadTask.cancel();
mReloadTask = null;
}

final long messageReloadIntervalMs =
mAutoMessageReloadScheduler.getMessageReloadIntervalMs();

// Zero means no scheduled executions.
if (messageReloadIntervalMs == 0) {
Log.d(TAG, "Disabling message auto-reloading");
return;
}

// Schedule the tasks.
mReloadTask = new ReloadMessagesTask();
Log.d(TAG, String.format("Scheduled to run at intervals of %dms", messageReloadIntervalMs));
mTimer.schedule(mReloadTask, 0, messageReloadIntervalMs);
}

class ReloadMessagesTask extends TimerTask {
private Disposable mDisposable;

@Override
public void run() {
dispose();
Log.d(TAG, "Auto-reloading messages");
mDisposable = mAutoMessageReloadScheduler.reloadMessagesInAllAccounts()
.onErrorComplete()
.subscribe(() -> Log.d(TAG, "Auto-reloading messages completed."));
}

@Override
public boolean cancel() {
dispose();
return super.cancel();
}

private void dispose() {
if (mDisposable != null) {
mDisposable.dispose();
mDisposable = null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,7 @@ public void onReceive(Context context, Intent intent) {
return;
}

// Start the WSAndroidService.
// FIXME(saemy): Start in a JobScheduler as it needs to be started in the foreground?
Intent serviceIntent = new Intent(context, WSAndroidService.class);
context.startService(serviceIntent);

// JobScheduler js = new JobScheduler() {
// @Override public int schedule(@NonNull JobInfo jobInfo) {
// return 0;
// }
//
// @Override
// public int enqueue(@NonNull JobInfo jobInfo, @NonNull JobWorkItem jobWorkItem) {
// return 0;
// }
//
// @Override public void cancel(int i) {
//
// }
//
// @Override public void cancelAll() {
//
// }
//
// @NonNull @Override public List<JobInfo> getAllPendingJobs() {
// return null;
// }
//
// @Nullable @Override public JobInfo getPendingJob(int i) {
// return null;
// }
// }
// This leads to AutoMessageReloadScheduler being created.
WSAndroidApplication.getAppComponent().inject(this);
}
}
Loading

0 comments on commit 38b7eb1

Please sign in to comment.