summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoader.java
blob: 8a31c1ce0a87b4eb716e29df336da43c20b29a40 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.fxa;

import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import android.content.AsyncTaskLoader;
import android.support.v4.content.LocalBroadcastManager;

import java.lang.ref.WeakReference;

/**
 * A Loader that queries and updates based on the existence of Firefox and
 * legacy Sync Android Accounts.
 *
 * The loader returns an Android Account (of either Account type) if an account
 * exists, and null to indicate no Account is present.
 *
 * The loader listens for Accounts added and deleted, and also Accounts being
 * updated by Sync or another Activity, via the use of
 * {@link AndroidFxAccount#setState(org.mozilla.gecko.fxa.login.State)}.
 * Be careful of message loops if you update the account state from an activity
 * that uses this loader.
 *
 * This implementation is based on
 * <a href="http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html">http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html</a>.
 */
public class AccountLoader extends AsyncTaskLoader<Account> {
  protected Account account = null;
  protected BroadcastReceiver broadcastReceiver = null;

  // Hold a weak reference to AccountLoader instance in this Runnable to avoid potentially leaking it
  // after posting to a Handler in the BroadcastReceiver returned from makeNewObserver.
  private final BroadcastReceiverRunnable broadcastReceiverRunnable = new BroadcastReceiverRunnable(this);

  public AccountLoader(final Context context) {
    super(context);
  }

  // Task that performs the asynchronous load.
  @Override
  public Account loadInBackground() {
    return FirefoxAccounts.getFirefoxAccount(getContext());
  }

  // Deliver the results to the registered listener.
  @Override
  public void deliverResult(Account data) {
    if (isReset()) {
      // The Loader has been reset; ignore the result and invalidate the data.
      releaseResources(data);
      return;
    }

    // Hold a reference to the old data so it doesn't get garbage collected.
    // We must protect it until the new data has been delivered.
    Account oldData = account;
    account = data;

    if (isStarted()) {
      // If the Loader is in a started state, deliver the results to the
      // client. The superclass method does this for us.
      super.deliverResult(data);
    }

    // Invalidate the old data as we don't need it any more.
    if (oldData != null && oldData != data) {
      releaseResources(oldData);
    }
  }

  // The Loader’s state-dependent behavior.
  @Override
  protected void onStartLoading() {
    if (account != null) {
      // Deliver any previously loaded data immediately.
      deliverResult(account);
    }

    // Begin monitoring the underlying data source.
    if (broadcastReceiver == null) {
      broadcastReceiver = makeNewObserver();
      registerLocalObserver(getContext(), broadcastReceiver);
      registerSystemObserver(getContext(), broadcastReceiver);
    }

    if (takeContentChanged() || account == null) {
      // When the observer detects a change, it should call onContentChanged()
      // on the Loader, which will cause the next call to takeContentChanged()
      // to return true. If this is ever the case (or if the current data is
      // null), we force a new load.
      forceLoad();
    }
  }

  @Override
  protected void onStopLoading() {
    // The Loader is in a stopped state, so we should attempt to cancel the
    // current load (if there is one).
    cancelLoad();

    // Note that we leave the observer as is. Loaders in a stopped state
    // should still monitor the data source for changes so that the Loader
    // will know to force a new load if it is ever started again.
  }

  @Override
  protected void onReset() {
    // Ensure the loader has been stopped.  In CursorLoader and the template
    // this code follows (see the class comment), this is onStopLoading, which
    // appears to not set the started flag (see Loader itself).
    stopLoading();

    // At this point we can release the resources associated with 'mData'.
    if (account != null) {
      releaseResources(account);
      account = null;
    }

    // The Loader is being reset, so we should stop monitoring for changes.
    if (broadcastReceiver != null) {
      final BroadcastReceiver observer = broadcastReceiver;
      broadcastReceiver = null;
      unregisterObserver(getContext(), observer);
    }
  }

  @Override
  public void onCanceled(final Account data) {
    // Attempt to cancel the current asynchronous load.
    super.onCanceled(data);

    // The load has been canceled, so we should release the resources
    // associated with 'data'.
    releaseResources(data);
  }

  // Observer which receives notifications when the data changes.
  protected BroadcastReceiver makeNewObserver() {
    return new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        // onContentChanged must be called on the main thread.
        // If we're already on the main thread, call it directly.
        if (Looper.myLooper() == Looper.getMainLooper()) {
          onContentChanged();
          return;
        }

        // Otherwise, post a Runnable to a Handler bound to the main thread's message loop.
        final Handler mainHandler = new Handler(Looper.getMainLooper());
        mainHandler.post(broadcastReceiverRunnable);
      }
    };
  }

  private static class BroadcastReceiverRunnable implements Runnable {
    private final WeakReference<AccountLoader> accountLoaderWeakReference;

    public BroadcastReceiverRunnable(final AccountLoader accountLoader) {
      accountLoaderWeakReference = new WeakReference<>(accountLoader);
    }

    @Override
    public void run() {
      final AccountLoader accountLoader = accountLoaderWeakReference.get();
      if (accountLoader != null) {
        accountLoader.onContentChanged();
      }
    }
  }

  private void releaseResources(Account data) {
    // For a simple List, there is nothing to do. For something like a Cursor, we
    // would close it in this method. All resources associated with the Loader
    // should be released here.
  }

  /**
   * Register provided observer with the LocalBroadcastManager to listen for internal events.
   *
   * @param context <code>Context</code> to use for obtaining LocalBroadcastManager instance.
   * @param observer <code>BroadcastReceiver</code> which will handle local events.
   */
  protected static void registerLocalObserver(final Context context, final BroadcastReceiver observer) {
    final IntentFilter intentFilter = new IntentFilter();
    // Firefox Account internal state changed.
    intentFilter.addAction(FxAccountConstants.ACCOUNT_STATE_CHANGED_ACTION);
    // Firefox Account profile state changed.
    intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION);

    LocalBroadcastManager.getInstance(context).registerReceiver(observer, intentFilter);
  }

  /**
   * Register provided observer for handling system-wide broadcasts.
   *
   * @param context <code>Context</code> to use for registering a receiver.
   * @param observer <code>BroadcastReceiver</code> which will handle system events.
   */
  protected static void registerSystemObserver(final Context context, final BroadcastReceiver observer) {
    context.registerReceiver(observer,
            // Android Account added or removed.
            new IntentFilter(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION),
            // No broadcast permissions required.
            null,
            // Null handler ensures that broadcasts will be handled on the main thread.
            null
    );
  }

  protected static void unregisterObserver(final Context context, final BroadcastReceiver observer) {
    LocalBroadcastManager.getInstance(context).unregisterReceiver(observer);
    context.unregisterReceiver(observer);
  }
}