individual-aggregator.vala 78.8 KB
Newer Older
1
2
/*
 * Copyright (C) 2010 Collabora Ltd.
3
 * Copyright (C) 2012 Philip Withnall
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 *
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *       Travis Reitter <travis.reitter@collabora.co.uk>
20
 *       Philip Withnall <philip@tecnocode.co.uk>
21
22
 */

23
using Gee;
24
using GLib;
25

26
27
28
/**
 * Errors from {@link IndividualAggregator}s.
 */
29
30
public errordomain Folks.IndividualAggregatorError
{
31
32
33
  /**
   * Adding a {@link Persona} to a {@link PersonaStore} failed.
   */
34
  ADD_FAILED,
35
36
37
38

  /**
   * An operation which required the use of a writeable store failed because no
   * writeable store was available.
39
40
   *
   * @since 0.1.13
41
   */
42
43
  [Deprecated (since = "0.6.2.1",
      replacement = "IndividualAggregatorError.NO_PRIMARY_STORE")]
44
  NO_WRITEABLE_STORE,
45
46
47
48
49
50
51

  /**
   * The {@link PersonaStore} was offline (ie, this is a temporary failure).
   *
   * @since 0.3.0
   */
  STORE_OFFLINE,
52
53
54
55
56
57

  /**
   * The {@link PersonaStore} did not support writing to a property which the
   * user requested to write to, or which was necessary to write to for storing
   * linking information.
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
58
   * @since 0.6.2
59
60
   */
  PROPERTY_NOT_WRITEABLE,
61
62
63
64
65

  /**
   * An operation which required the use of a primary store failed because no
   * primary store was available.
   *
Philip Withnall's avatar
Philip Withnall committed
66
   * @since 0.6.3
67
68
   */
  NO_PRIMARY_STORE,
69
70
}

71
/**
72
 * Stores {@link Individual}s which have been created through
Philip Withnall's avatar
Philip Withnall committed
73
 * aggregation of all the {@link Persona}s provided by the various
74
75
76
 * {@link Backend}s.
 *
 * This is the main interface for client applications.
77
 */
78
public class Folks.IndividualAggregator : Object
79
{
80
81
  private BackendStore _backend_store;
  private HashMap<string, PersonaStore> _stores;
82
  private unowned PersonaStore? _primary_store = null;
83
  private HashSet<Backend> _backends;
84
  private HashMultiMap<string, Individual> _link_map;
85
  private bool _linking_enabled = true;
86
  private bool _is_prepared = false;
87
  private bool _prepare_pending = false;
88
  private Debug _debug;
89
90
  private string _configured_primary_store_type_id;
  private string _configured_primary_store_id;
91
92
  private static const string _FOLKS_GSETTINGS_SCHEMA = "org.freedesktop.folks";
  private static const string _PRIMARY_STORE_CONFIG_KEY = "primary-store";
93

94
95
96
97
98
99
100
101
102
103
104
  /* The number of persona stores and backends we're waiting to become
   * quiescent. Once these both reach 0, we should be in a quiescent state.
   * We have to count both of them so that we can handle the case where one
   * backend becomes available, and its persona stores all become quiescent,
   * long before any other backend becomes available. In this case, we want
   * the aggregator to signal that it's reached a quiescent state only once
   * all the other backends have also become available. */
  private uint _non_quiescent_persona_store_count = 0;
  /* Same for backends. */
  private uint _non_quiescent_backend_count = 0;
  private bool _is_quiescent = false;
105
106
107
108
109
  /* As a precaution against backends/persona stores which never reach
   * quiescence (due to bugs), we implement a timeout after which we forcibly
   * reach quiescence. */
  private uint _quiescent_timeout_id = 0;

110
  private static const uint _QUIESCENT_TIMEOUT = 30; /* seconds */
111

112
  /* We use this to know if the primary PersonaStore has been explicitly
113
   * set by the user (either via GSettings or an env variable). If that is the
114
115
116
   * case, we don't want to override it with other PersonaStores that
   * announce themselves as default (i.e.: default address book from e-d-s). */
  private bool _user_configured_primary_store = false;
117

118
119
120
121
122
123
124
125
126
127
  /**
   * Whether {@link IndividualAggregator.prepare} has successfully completed for
   * this aggregator.
   *
   * @since 0.3.0
   */
  public bool is_prepared
    {
      get { return this._is_prepared; }
    }
128

129
130
131
132
133
134
135
136
137
138
  /**
   * Whether the aggregator has reached a quiescent state. This will happen at
   * some point after {@link IndividualAggregator.prepare} has successfully
   * completed for the aggregator. An aggregator is in a quiescent state when
   * all the {@link PersonaStore}s listed by its backends have reached a
   * quiescent state.
   *
   * It's guaranteed that this property's value will only ever change after
   * {@link IndividualAggregator.is_prepared} has changed to `true`.
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
139
   * @since 0.6.2
140
141
142
143
144
145
   */
  public bool is_quiescent
    {
      get { return this._is_quiescent; }
    }

146
147
148
149
150
151
  /**
   * Our configured primary (writeable) store.
   *
   * Which one to use is decided (in order or precedence)
   * by:
   *
152
   * - the FOLKS_PRIMARY_STORE env var (mostly for debugging)
153
   * - the GSettings key set in `_PRIMARY_STORE_CONFIG_KEY` (system set store)
154
   * - going with the `key-file` or `eds` store as the fall-back option
155
   *
Travis Reitter's avatar
Travis Reitter committed
156
   * @since 0.5.0
157
   */
158
  public PersonaStore? primary_store
159
    {
160
      get { return this._primary_store; }
161
162
    }

163
164
165
  private Map<string, Individual> _individuals;
  private Map<string, Individual> _individuals_ro;

166
  /**
167
   * A map from {@link Individual.id}s to their {@link Individual}s.
168
169
170
171
172
173
174
   *
   * This is the canonical set of {@link Individual}s provided by this
   * IndividualAggregator.
   *
   * {@link Individual}s may be added or removed using
   * {@link IndividualAggregator.add_persona_from_details} and
   * {@link IndividualAggregator.remove_individual}, respectively.
175
   *
Travis Reitter's avatar
Travis Reitter committed
176
   * @since 0.5.1
177
   */
178
179
180
181
182
183
184
185
186
  public Map<string, Individual> individuals
    {
      get { return this._individuals_ro; }
      private set
        {
          this._individuals = value;
          this._individuals_ro = this._individuals.read_only_view;
        }
    }
187

188
189
190
  /**
   * The {@link Individual} representing the user.
   *
191
   * If it exists, this holds the {@link Individual} who is the user: the
192
193
194
195
196
   * {@link Individual} containing the {@link Persona}s who are the owners of
   * the accounts for their respective backends.
   *
   * @since 0.3.0
   */
Philip Withnall's avatar
Philip Withnall committed
197
  public Individual? user { get; private set; }
198

199
  /**
200
201
   * Emitted when one or more {@link Individual}s are added to or removed from
   * the aggregator.
202
   *
203
204
205
206
207
   * If more information about the relationships between {@link Individual}s
   * which have been linked and unlinked is needed, consider connecting to
   * {@link IndividualAggregator.individuals_changed_detailed} instead, which is
   * emitted at the same time as this signal.
   *
208
209
210
   * This will not be emitted until after {@link IndividualAggregator.prepare}
   * has been called.
   *
211
   * @param added a list of {@link Individual}s which have been added
212
213
214
215
   * @param removed a list of {@link Individual}s which have been removed
   * @param message a string message from the backend, if any
   * @param actor the {@link Persona} who made the change, if known
   * @param reason the reason for the change
216
   *
Travis Reitter's avatar
Travis Reitter committed
217
   * @since 0.5.1
218
   */
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
219
  [Deprecated (since = "0.6.2",
220
      replacement = "IndividualAggregator.individuals_changed_detailed")]
221
222
  public signal void individuals_changed (Set<Individual> added,
      Set<Individual> removed,
223
224
      string? message,
      Persona? actor,
225
      GroupDetails.ChangeReason reason);
226

227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
  /**
   * Emitted when one or more {@link Individual}s are added to or removed from
   * the aggregator.
   *
   * This is emitted at the same time as
   * {@link IndividualAggregator.individuals_changed}, but includes more
   * information about the relationships between {@link Individual}s which have
   * been linked and unlinked.
   *
   * Individuals which have been linked will be listed in the multi-map as
   * mappings from the old individuals to the single new individual which
   * replaces them (i.e. each of the old individuals will map to the same new
   * individual). This new individual is the one which will be specified as the
   * `replacement_individual` in the {@link Individual.removed} signal for the
   * old individuals.
   *
   * Individuals which have been unlinked will be listed in the multi-map as
   * a mapping from the unlinked individual to a set of one or more individuals
   * which replace it.
   *
   * Individuals which have been added will be listed in the multi-map as a
   * mapping from `null` to the set of added individuals. If `null` doesn't
   * map to anything, no individuals have been added to the aggregator.
   *
   * Individuals which have been removed will be listed in the multi-map as
   * mappings from the removed individual to `null`.
   *
   * This will not be emitted until after {@link IndividualAggregator.prepare}
   * has been called.
   *
257
   * @param changes a mapping of old {@link Individual}s to new
258
259
260
   * {@link Individual}s for the individuals which have changed in the
   * aggregator
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
261
   * @since 0.6.2
262
263
264
265
   */
  public signal void individuals_changed_detailed (
      MultiMap<Individual?, Individual?> changes);

266
  /* FIXME: make this a singleton? */
267
268
269
270
  /**
   * Create a new IndividualAggregator.
   *
   * Clients should connect to the
271
272
273
   * {@link IndividualAggregator.individuals_changed} signal (or the
   * {@link IndividualAggregator.individuals_changed_detailed} signal), then
   * call {@link IndividualAggregator.prepare} to load the backends and start
274
   * aggregating individuals.
275
   *
276
277
278
   * An example of how to set up an IndividualAggregator:
   * {{{
   *   IndividualAggregator agg = new IndividualAggregator ();
279
   *   agg.individuals_changed_detailed.connect (individuals_changed_cb);
280
281
   *   agg.prepare ();
   * }}}
282
   */
283
  public IndividualAggregator ()
284
285
286
287
288
    {
      Object ();
    }

  construct
289
    {
290
      this._stores = new HashMap<string, PersonaStore> ();
291
292
      this._individuals = new HashMap<string, Individual> ();
      this._individuals_ro = this._individuals.read_only_view;
293
      this._link_map = new HashMultiMap<string, Individual> ();
294

295
      this._backends = new HashSet<Backend> ();
296
297
      this._debug = Debug.dup ();
      this._debug.print_status.connect (this._debug_print_status);
298

299
300
301
302
303
304
305
306
307
308
309
310
311
      /* Check out the configured primary store */
      var store_config_ids = Environment.get_variable ("FOLKS_PRIMARY_STORE");
      if (store_config_ids == null)
        {
          store_config_ids = Environment.get_variable ("FOLKS_WRITEABLE_STORE");
          if (store_config_ids != null)
            {
              var deprecated_warn = "FOLKS_WRITEABLE_STORE is deprecated, ";
              deprecated_warn += "use FOLKS_PRIMARY_STORE";
              warning (deprecated_warn);
            }
        }

312
      if (store_config_ids != null)
313
        {
314
          debug ("Setting primary store IDs from environment variable.");
Philip Withnall's avatar
Philip Withnall committed
315
          this._configure_primary_store ((!) store_config_ids);
316
317
318
        }
      else
        {
319
          debug ("Setting primary store IDs to defaults.");
320
321
322
          if (BuildConf.HAVE_EDS)
            {
              this._configured_primary_store_type_id = "eds";
323
              this._configured_primary_store_id = "system-address-book";
324
325
326
327
328
329
            }
          else
            {
              this._configured_primary_store_type_id = "key-file";
              this._configured_primary_store_id = "";
            }
330

331
332
333
          var settings = new Settings (this._FOLKS_GSETTINGS_SCHEMA);
          var val = settings.get_string (this._PRIMARY_STORE_CONFIG_KEY);
          if (val != null && val != "")
334
            {
335
336
              debug ("Setting primary store IDs from GSettings.");
              this._configure_primary_store ((!) val);
337
338
339
            }
        }

340
341
342
343
      debug ("Primary store IDs are '%s' and '%s'.",
          this._configured_primary_store_type_id,
          this._configured_primary_store_id);

344
      var disable_linking = Environment.get_variable ("FOLKS_DISABLE_LINKING");
345
      if (disable_linking != null)
Philip Withnall's avatar
Philip Withnall committed
346
        disable_linking = ((!) disable_linking).strip ().down ();
347
      this._linking_enabled = (disable_linking == null ||
348
349
          disable_linking == "no" || disable_linking == "0");

350
      this._backend_store = BackendStore.dup ();
351
352

      debug ("Constructing IndividualAggregator %p", this);
353
354
    }

355
356
  ~IndividualAggregator ()
    {
357
358
      debug ("Destroying IndividualAggregator %p", this);

359
360
361
362
363
364
      if (this._quiescent_timeout_id != 0)
        {
          Source.remove (this._quiescent_timeout_id);
          this._quiescent_timeout_id = 0;
        }

365
366
      this._backend_store.backend_available.disconnect (
          this._backend_available_cb);
367
368
369
370

      this._debug.print_status.disconnect (this._debug_print_status);
    }

371
  private void _configure_primary_store (string store_config_ids)
372
    {
373
      debug ("_configure_primary_store to '%s'", store_config_ids);
374
375
      this._user_configured_primary_store = true;

376
      if (store_config_ids.index_of (":") != -1)
377
378
        {
          var ids = store_config_ids.split (":", 2);
379
380
          this._configured_primary_store_type_id = ids[0];
          this._configured_primary_store_id = ids[1];
381
382
383
        }
      else
        {
384
385
          this._configured_primary_store_type_id = store_config_ids;
          this._configured_primary_store_id = "";
386
387
388
        }
    }

389
390
391
392
393
394
395
396
  private void _debug_print_status (Debug debug)
    {
      const string domain = Debug.STATUS_LOG_DOMAIN;
      const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;

      debug.print_heading (domain, level, "IndividualAggregator (%p)", this);
      debug.print_key_value_pairs (domain, level,
          "Ref. count", this.ref_count.to_string (),
397
          "Primary store", "%p".printf (this._primary_store),
398
399
          "Configured store type id", this._configured_primary_store_type_id,
          "Configured store id", this._configured_primary_store_id,
400
          "Linking enabled?", this._linking_enabled ? "yes" : "no",
401
402
403
404
405
406
          "Prepared?", this._is_prepared ? "yes" : "no",
          "Quiescent?", this._is_quiescent
              ? "yes"
              : "no (%u backends, %u persona stores left)".printf (
                  this._non_quiescent_backend_count,
                  this._non_quiescent_persona_store_count)
407
408
409
410
411
412
413
414
      );

      debug.print_line (domain, level,
          "%u Individuals:", this.individuals.size);
      debug.indent ();

      foreach (var individual in this.individuals.values)
        {
Philip Withnall's avatar
Philip Withnall committed
415
          string? trust_level = null;
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458

          switch (individual.trust_level)
            {
              case TrustLevel.NONE:
                trust_level = "none";
                break;
              case TrustLevel.PERSONAS:
                trust_level = "personas";
                break;
              default:
                assert_not_reached ();
            }

          debug.print_heading (domain, level, "Individual (%p)", individual);
          debug.print_key_value_pairs (domain, level,
              "Ref. count", individual.ref_count.to_string (),
              "ID", individual.id,
              "User?", individual.is_user ? "yes" : "no",
              "Trust level", trust_level
          );
          debug.print_line (domain, level, "%u Personas:",
              individual.personas.size);

          debug.indent ();

          foreach (var persona in individual.personas)
            {
              debug.print_heading (domain, level, "Persona (%p)", persona);
              debug.print_key_value_pairs (domain, level,
                  "Ref. count", persona.ref_count.to_string (),
                  "UID", persona.uid,
                  "IID", persona.iid,
                  "Display ID", persona.display_id,
                  "User?", persona.is_user ? "yes" : "no"
              );
            }

          debug.unindent ();
        }

      debug.unindent ();

      debug.print_line (domain, level, "%u entries in the link map:",
459
          this._link_map.size);
460
461
      debug.indent ();

462
      foreach (var link_key in this._link_map.get_keys ())
463
        {
464
465
466
467
468
469
470
471
472
473
          debug.print_line (domain, level, "%s → {", link_key);
          debug.indent ();

          foreach (var individual in this._link_map.get (link_key))
            {
              debug.print_line (domain, level, "%p", individual);
            }

          debug.unindent ();
          debug.print_line (domain, level, "}");
474
475
476
477
478
        }

      debug.unindent ();

      debug.print_line (domain, level, "");
479
480
    }

481
482
483
484
485
  /**
   * Prepare the IndividualAggregator for use.
   *
   * This loads all the available backends and prepares them for use by the
   * IndividualAggregator. This should be called //after// connecting to the
486
487
488
489
   * {@link IndividualAggregator.individuals_changed} signal (or
   * {@link IndividualAggregator.individuals_changed_detailed} signal), or a
   * race condition could occur, with the signal being emitted before your code
   * has connected to them, and {@link Individual}s getting "lost" as a result.
490
   *
491
492
   * This function is guaranteed to be idempotent (since version 0.3.0).
   *
493
494
495
496
497
498
   * Concurrent calls to this function from different threads will block until
   * preparation has completed. However, concurrent calls to this function from
   * a single thread might not, i.e. the first call will block but subsequent
   * calls might return before the first one. (Though they will be safe in every
   * other respect.)
   *
499
500
501
   * @throws GLib.Error if preparing any of the backends failed — this error
   * will be passed through from {@link BackendStore.load_backends}
   *
502
   * @since 0.1.11
503
   */
504
  public async void prepare () throws GLib.Error
505
    {
506
507
      Internal.profiling_start ("preparing IndividualAggregator");

508
509
510
      /* Once this async function returns, all the {@link Backend}s will have
       * been prepared (though no {@link PersonaStore}s are guaranteed to be
       * available yet). This last guarantee is new as of version 0.2.0. */
511
512
513

      lock (this._is_prepared)
        {
514
515
516
517
518
519
          if (this._is_prepared || this._prepare_pending)
            {
              return;
            }

          try
520
            {
521
              this._prepare_pending = true;
522

523
524
525
526
527
              /* Temporarily increase the non-quiescent backend count so that
               * we don't prematurely reach quiescence due to odd timing of the
               * backend-available signals. */
              this._non_quiescent_backend_count++;

528
529
530
531
532
533
534
535
536
537
538
539
540
541
              this._backend_store.backend_available.connect (
                  this._backend_available_cb);

              /* Load any backends which already exist. This could happen if the
               * BackendStore has stayed alive after being used by a previous
               * IndividualAggregator instance. */
              var backends = this._backend_store.enabled_backends.values;
              foreach (var backend in backends)
                {
                  this._backend_available_cb (this._backend_store, backend);
                }

              /* Load any backends which haven't been loaded already. (Typically
               * all of them.) */
542
              yield this._backend_store.load_backends ();
543

544
545
              this._non_quiescent_backend_count--;

546
547
              this._is_prepared = true;
              this.notify_property ("is-prepared");
548
549
550
551
552
553
554
555
556

              /* Mark the aggregator as having reached a quiescent state if
               * appropriate. This will typically only happen here in cases
               * where the stores were all prepared and quiescent before the
               * aggregator was created. */
              if (this._is_quiescent == false)
                {
                  this._notify_if_is_quiescent ();
                }
557
            }
558
559
560
561
          finally
            {
              this._prepare_pending = false;
            }
562
        }
563
564

      Internal.profiling_end ("preparing IndividualAggregator");
565
566
    }

567
568
569
  /**
   * Get all matches for a given {@link Individual}.
   *
570
571
572
573
574
575
   * @param matchee the individual to find matches for
   * @param min_threshold the threshold for accepting a match
   * @return a map from matched individuals to the degree with which they match
   * `matchee` (which is guaranteed to at least equal `min_threshold`);
   * if no matches could be found, an empty map is returned
   *
Travis Reitter's avatar
Travis Reitter committed
576
   * @since 0.5.1
577
   */
578
  public Map<Individual, MatchResult> get_potential_matches
579
580
581
582
583
584
      (Individual matchee, MatchResult min_threshold = MatchResult.VERY_HIGH)
    {
      HashMap<Individual, MatchResult> matches =
          new HashMap<Individual, MatchResult> ();
      Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();

585
      foreach (var i in this._individuals.values)
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
        {
          if (i.id == matchee.id)
                continue;

          var result = matchObj.potential_match (i, matchee);
          if (result >= min_threshold)
            {
              matches.set (i, result);
            }
        }

      return matches;
    }

  /**
   * Get all combinations between all {@link Individual}s.
   *
603
604
605
606
607
608
   * @param min_threshold the threshold for accepting a match
   * @return a map from each individual in the aggregator to a map of the
   * other individuals in the aggregator which can be matched with that
   * individual, mapped to the degree with which they match the original
   * individual (which is guaranteed to at least equal `min_threshold`)
   *
Travis Reitter's avatar
Travis Reitter committed
609
   * @since 0.5.1
610
   */
611
  public Map<Individual, Map<Individual, MatchResult>>
612
613
614
615
616
      get_all_potential_matches
        (MatchResult min_threshold = MatchResult.VERY_HIGH)
    {
      HashMap<Individual, HashMap<Individual, MatchResult>> matches =
        new HashMap<Individual, HashMap<Individual, MatchResult>> ();
617
      var individuals = this._individuals.values.to_array ();
618
619
      Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();

620
      for (var i = 0; i < individuals.length; i++)
621
        {
622
          var a = individuals[i];
Philip Withnall's avatar
Philip Withnall committed
623
624
625
626

          HashMap<Individual, MatchResult>? _matches_a = matches.get (a);
          HashMap<Individual, MatchResult> matches_a;
          if (_matches_a == null)
627
628
629
630
            {
              matches_a = new HashMap<Individual, MatchResult> ();
              matches.set (a, matches_a);
            }
Philip Withnall's avatar
Philip Withnall committed
631
632
633
634
          else
            {
              matches_a = (!) _matches_a;
            }
635

636
          for (var f = i + 1; f < individuals.length; f++)
637
            {
638
              var b = individuals[f];
Philip Withnall's avatar
Philip Withnall committed
639
640
641
642

              HashMap<Individual, MatchResult>? _matches_b = matches.get (b);
              HashMap<Individual, MatchResult> matches_b;
              if (_matches_b == null)
643
644
645
646
                {
                  matches_b = new HashMap<Individual, MatchResult> ();
                  matches.set (b, matches_b);
                }
Philip Withnall's avatar
Philip Withnall committed
647
648
649
650
              else
                {
                  matches_b = (!) _matches_b;
                }
651
652
653
654
655
656

              var result = matchObj.potential_match (a, b);

              if (result >= min_threshold)
                {
                  matches_a.set (b, result);
657
                  matches_b.set (a, result);
658
659
660
661
662
663
664
                }
            }
        }

      return matches;
    }

665
  private async void _add_backend (Backend backend)
666
    {
667
      if (!this._backends.contains (backend))
668
        {
669
          this._backends.add (backend);
670
671

          backend.persona_store_added.connect (
672
              this._backend_persona_store_added_cb);
673
          backend.persona_store_removed.connect (
674
              this._backend_persona_store_removed_cb);
675
676
          backend.notify["is-quiescent"].connect (
              this._backend_is_quiescent_changed_cb);
677

678
679
680
681
682
          /* Handle the stores that have already been signaled. Since
           * this might change while we are looping, get a copy first.
           */
          var stores = backend.persona_stores.values.to_array ();
          foreach (var persona_store in stores)
683
              {
684
685
                this._backend_persona_store_added_cb (backend, persona_store);
              }
686
        }
687
    }
688

689
  private void _backend_available_cb (BackendStore backend_store,
690
      Backend backend)
691
    {
692
693
694
695
696
697
698
699
      /* Increase the number of non-quiescent backends we're waiting for.
       * If we've already reached a quiescent state, this is ignored. If we
       * haven't, this delays us reaching a quiescent state until the
       * _backend_is_quiescent_changed_cb() callback is called for this
       * backend. */
      if (backend.is_quiescent == false)
        {
          this._non_quiescent_backend_count++;
700
701
702
703
704
705
706
707
708

          /* Start the timeout to force quiescence if the backend (or its
           * persona stores) misbehave and don't reach quiescence. */
          if (this._quiescent_timeout_id == 0)
            {
              this._quiescent_timeout_id =
                  Timeout.add_seconds (this._QUIESCENT_TIMEOUT,
                      this._quiescent_timeout_cb);
            }
709
710
        }

711
      this._add_backend.begin (backend);
Travis Reitter's avatar
Travis Reitter committed
712
713
    }

714
  private void _set_primary_store (PersonaStore store)
Travis Reitter's avatar
Travis Reitter committed
715
    {
716
717
      debug ("_set_primary_store()");

718
719
      if (this._primary_store == store)
        return;
720

721
      /* We use the configured PersonaStore as the primary PersonaStore.
722
723
724
725
726
       *
       * If the type_id is `eds` we *must* know the actual store
       * (address book) we are talking about or we might end up using
       * a random store on every run.
       */
727
      if (store.type_id == this._configured_primary_store_type_id)
728
        {
729
          if ((store.type_id != "eds" &&
730
731
                  this._configured_primary_store_id == "") ||
              this._configured_primary_store_id == store.id)
732
            {
733
734
735
              debug ("Setting primary store to %p (type ID: %s, ID: %s)",
                  store, store.type_id, store.id);

736
              var previous_store = this._primary_store;
737
              this._primary_store = store;
738

Philip Withnall's avatar
Philip Withnall committed
739
              store.freeze_notify ();
740
741
              if (previous_store != null)
                {
Philip Withnall's avatar
Philip Withnall committed
742
743
                  ((!) previous_store).freeze_notify ();
                  ((!) previous_store).is_primary_store = false;
744
                }
Philip Withnall's avatar
Philip Withnall committed
745
              store.is_primary_store = true;
746
              if (previous_store != null)
Philip Withnall's avatar
Philip Withnall committed
747
748
                ((!) previous_store).thaw_notify ();
              store.thaw_notify ();
749

750
              this.notify_property ("primary-store");
751
            }
752
        }
753
754
755
756
757
    }

  private void _backend_persona_store_added_cb (Backend backend,
      PersonaStore store)
    {
758
759
760
      debug ("_backend_persona_store_added_cb(): backend: %s, store: %s (%p)",
          backend.name, store.id, store);

761
762
      var store_id = this._get_store_full_id (store.type_id, store.id);

763
      this._maybe_configure_as_primary (store);
764
      this._set_primary_store (store);
765

766
      this._stores.set (store_id, store);
767
      store.personas_changed.connect (this._personas_changed_cb);
768
769
      store.notify["is-primary-store"].connect (
          this._is_primary_store_changed_cb);
770
771
      store.notify["is-quiescent"].connect (
          this._persona_store_is_quiescent_changed_cb);
772
773
      store.notify["is-user-set-default"].connect (
          this._persona_store_is_user_set_default_changed_cb);
774
775
776
777
778
779
780
781
782

      /* Increase the number of non-quiescent persona stores we're waiting for.
       * If we've already reached a quiescent state, this is ignored. If we
       * haven't, this delays us reaching a quiescent state until the
       * _persona_store_is_quiescent_changed_cb() callback is called for this
       * store. */
      if (store.is_quiescent == false)
        {
          this._non_quiescent_persona_store_count++;
783
784
785
786
787
788
789
790
791

          /* Start the timeout to force quiescence if the backend (or its
           * persona stores) misbehave and don't reach quiescence. */
          if (this._quiescent_timeout_id == 0)
            {
              this._quiescent_timeout_id =
                  Timeout.add_seconds (this._QUIESCENT_TIMEOUT,
                      this._quiescent_timeout_cb);
            }
792
        }
793

794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
      /* Handle any pre-existing personas in the store. This can happen if the
       * store existed (and was prepared) before this IndividualAggregator was
       * constructed. */
      if (store.personas.size > 0)
        {
          var persona_set = new HashSet<Persona> ();
          foreach (var p in store.personas.values)
            {
              persona_set.add (p);
            }

          this._personas_changed_cb (store, persona_set,
              new HashSet<Persona> (), null, null,
              GroupDetails.ChangeReason.NONE);
        }

      /* Prepare the store and receive a load of other personas-changed
       * signals. */
812
813
814
815
816
817
818
819
      store.prepare.begin ((obj, result) =>
        {
          try
            {
              store.prepare.end (result);
            }
          catch (GLib.Error e)
            {
820
821
822
              /* Translators: the first parameter is a persona store identifier
               * and the second is an error message. */
              warning (_("Error preparing persona store '%s': %s"), store_id,
823
824
825
                  e.message);
            }
        });
Travis Reitter's avatar
Travis Reitter committed
826
827
    }

828
  private void _backend_persona_store_removed_cb (Backend backend,
829
      PersonaStore store)
Travis Reitter's avatar
Travis Reitter committed
830
    {
831
      store.personas_changed.disconnect (this._personas_changed_cb);
832
833
      store.notify["is-quiescent"].disconnect (
          this._persona_store_is_quiescent_changed_cb);
834
835
      store.notify["is-primary-store"].disconnect (
          this._is_primary_store_changed_cb);
836
837
      store.notify["is-user-set-default"].disconnect (
          this._persona_store_is_user_set_default_changed_cb);
Travis Reitter's avatar
Travis Reitter committed
838

839
840
841
842
843
844
845
846
      /* If we were still waiting on this persona store to reach a quiescent
       * state, stop waiting. */
      if (this._is_quiescent == false && store.is_quiescent == false)
        {
          this._non_quiescent_persona_store_count--;
          this._notify_if_is_quiescent ();
        }

847
      /* no need to remove this store's personas from all the individuals, since
Travis Reitter's avatar
Travis Reitter committed
848
849
850
       * they'll do that themselves (and emit their own 'removed' signal if
       * necessary) */

851
      if (this._primary_store == store)
852
        {
853
854
          debug ("Unsetting primary store as store %p (type ID: %s, ID: %s) " +
              "has been removed", store, store.type_id, store.id);
855
          this._primary_store = null;
856
857
          this.notify_property ("primary-store");
        }
858
      this._stores.unset (this._get_store_full_id (store.type_id, store.id));
859
860
    }

861
  private string _get_store_full_id (string type_id, string id)
862
    {
863
      return type_id + ":" + id;
864
    }
865

866
867
868
869
870
  /* Emit the individuals-changed signal ensuring that null parameters are
   * turned into empty sets, and both sets passed to signal handlers are
   * read-only. */
  private void _emit_individuals_changed (Set<Individual>? added,
      Set<Individual>? removed,
871
      MultiMap<Individual?, Individual?>? changes,
872
873
874
875
      string? message = null,
      Persona? actor = null,
      GroupDetails.ChangeReason reason = GroupDetails.ChangeReason.NONE)
    {
Philip Withnall's avatar
Philip Withnall committed
876
877
878
      Set<Individual> _added;
      Set<Individual> _removed;
      MultiMap<Individual?, Individual?> _changes;
879

Philip Withnall's avatar
Philip Withnall committed
880
881
882
      if ((added == null || ((!) added).size == 0) &&
          (removed == null || ((!) removed).size == 0) &&
          (changes == null || ((!) changes).size == 0))
883
884
885
886
        {
          /* Don't bother emitting it if nothing's changed */
          return;
        }
Philip Withnall's avatar
Philip Withnall committed
887

888
889
890
      Internal.profiling_point ("emitting " +
          "IndividualAggregator::individuals-changed");

Philip Withnall's avatar
Philip Withnall committed
891
892
893
894
      _added = (added != null) ? (!) added : new HashSet<Individual> ();
      _removed = (removed != null) ? (!) removed : new HashSet<Individual> ();

      if (changes != null)
895
        {
Philip Withnall's avatar
Philip Withnall committed
896
          _changes = (!) changes;
897
        }
Philip Withnall's avatar
Philip Withnall committed
898
      else
899
900
901
        {
          _changes = new HashMultiMap<Individual?, Individual?> ();
        }
902

903
904
905
906
907
908
909
910
911
912
913
      /* Debug output. */
      if (this._debug.debug_output_enabled == true)
        {
          debug ("Emitting individuals-changed-detailed with %u mappings:",
              _changes.size);

          foreach (var removed_ind in _changes.get_keys ())
            {
              foreach (var added_ind in _changes.get (removed_ind))
                {
                  debug ("    %s (%p) → %s (%p)",
Philip Withnall's avatar
Philip Withnall committed
914
915
916
                      (removed_ind != null) ? ((!) removed_ind).id : "",
                      removed_ind,
                      (added_ind != null) ? ((!) added_ind).id : "", added_ind);
917
918
919
920
921

                  if (removed_ind != null)
                    {
                      debug ("      Removed individual's personas:");

Philip Withnall's avatar
Philip Withnall committed
922
                      foreach (var p in ((!) removed_ind).personas)
923
924
925
926
927
928
929
930
931
                        {
                          debug ("        %s (%p)", p.uid, p);
                        }
                    }

                  if (added_ind != null)
                    {
                      debug ("      Added individual's personas:");

Philip Withnall's avatar
Philip Withnall committed
932
                      foreach (var p in ((!) added_ind).personas)
933
934
935
936
937
938
939
940
                        {
                          debug ("        %s (%p)", p.uid, p);
                        }
                    }
                }
            }
        }

941
942
      this.individuals_changed (_added.read_only_view, _removed.read_only_view,
          message, actor, reason);
943
      this.individuals_changed_detailed (_changes);
944
945
    }

946
947
948
  private void _connect_to_individual (Individual individual)
    {
      individual.removed.connect (this._individual_removed_cb);
949
      this._individuals.set (individual.id, individual);
950
951
952
953
    }

  private void _disconnect_from_individual (Individual individual)
    {
954
      this._individuals.unset (individual.id);
955
956
957
      individual.removed.disconnect (this._individual_removed_cb);
    }

Philip Withnall's avatar
Philip Withnall committed
958
  private void _add_personas (Set<Persona> added, ref Individual? user,
959
      ref HashMultiMap<Individual?, Individual?> individuals_changes)
960
    {
961
      foreach (var persona in added)
962
        {
963
          PersonaStoreTrust trust_level = persona.store.trust_level;
964
965

          /* These are the Individuals whose Personas will be linked together
966
           * to form the `final_individual`.
967
968
969
           * Since a given Persona can only be part of one Individual, and the
           * code in Persona._set_personas() ensures that there are no duplicate
           * Personas in a given Individual, ensuring that there are no
970
971
972
973
           * duplicate Individuals in `candidate_inds` (by using a
           * HashSet) guarantees that there will be no duplicate Personas
           * in the `final_individual`. */
          HashSet<Individual> candidate_inds = new HashSet<Individual> ();
974

975
          var final_personas = new HashSet<Persona> ();
976
977

          debug ("Aggregating persona '%s' on '%s'.", persona.uid, persona.iid);
978

979
980
          /* If the Persona is the user, we *always* want to link it to the
           * existing this.user. */
981
982
          if (persona.is_user == true && user != null &&
              ((!) user).has_anti_link_with_persona (persona) == false)
983
            {
Philip Withnall's avatar
Philip Withnall committed
984
985
986
              debug ("    Found candidate individual '%s' as user.",
                  ((!) user).id);
              candidate_inds.add ((!) user);
987
988
            }

989
990
991
992
          /* If we don't trust the PersonaStore at all, we can't link the
           * Persona to any existing Individual */
          if (trust_level != PersonaStoreTrust.NONE)
            {
993
994
              var candidate_ind_set = this._link_map.get (persona.iid);
              if (candidate_ind_set != null)
995
                {
996
997
998
999
                  foreach (var candidate_ind in candidate_ind_set)
                    {
                      if (candidate_ind != null &&
                          ((!) candidate_ind).trust_level != TrustLevel.NONE &&
1000
1001
                          ((!) candidate_ind).has_anti_link_with_persona (
                              persona) == false &&
1002
1003
1004
1005
1006
1007
                          candidate_inds.add ((!) candidate_ind))
                        {
                          debug ("    Found candidate individual '%s' by " +
                              "IID '%s'.", ((!) candidate_ind).id, persona.iid);
                        }
                    }
1008
                }
1009
            }
1010

1011
1012
          if (persona.store.trust_level == PersonaStoreTrust.FULL)
            {
1013
1014
              /* If we trust the PersonaStore the Persona came from, we can
               * attempt to link based on its linkable properties. */
1015
              foreach (unowned string foo in persona.linkable_properties)
1016
                {
1017
1018
1019
                  /* FIXME: If we just use string prop_name directly in the
                   * foreach, Vala doesn't copy it into the closure data, and
                   * prop_name ends up as NULL. bgo#628336 */
1020
                  unowned string prop_name = foo;
1021

1022
                  /* FIXME: can't be var because of bgo#638208 */
1023
1024
1025
                  unowned ObjectClass pclass = persona.get_class ();
                  if (pclass.find_property (prop_name) == null)
                    {
1026
1027
1028
1029
                      warning (
                          /* Translators: the parameter is a property name. */
                          _("Unknown property '%s' in linkable property list."),
                          prop_name);
1030
1031
                      continue;
                    }
1032

1033
1034
                  persona.linkable_property_to_links (prop_name, (l) =>
                    {
Travis Reitter's avatar
Travis Reitter committed
1035
                      unowned string prop_linking_value = l;
1036
1037
                      var candidate_ind_set =
                          this._link_map.get (prop_linking_value);
1038

1039
                      if (candidate_ind_set != null)
1040
                        {
1041
1042
1043
1044
1045
                          foreach (var candidate_ind in candidate_ind_set)
                            {
                              if (candidate_ind != null &&
                                  ((!) candidate_ind).trust_level !=
                                      TrustLevel.NONE &&
1046
1047
1048
                                  ((!) candidate_ind).
                                      has_anti_link_with_persona (
                                          persona) == false &&
1049
1050
1051
1052
1053
1054
1055
1056
                                  candidate_inds.add ((!) candidate_ind))
                                {
                                  debug ("    Found candidate individual '%s'" +
                                      " by linkable property '%s' = '%s'.",
                                      ((!) candidate_ind).id, prop_name,
                                      prop_linking_value);
                                }
                            }
1057
                        }
1058
1059
1060
                    });
                }
            }
1061

1062
1063
          /* Ensure the original persona makes it into the final individual */
          final_personas.add (persona);
1064

1065
          if (candidate_inds.size > 0 && this._linking_enabled == true)
1066
1067
1068
1069
1070
1071
            {
              /* The Persona's IID or linkable properties match one or more
               * linkable fields which are already in the link map, so we link
               * together all the Individuals we found to form a new
               * final_individual. Later, we remove the Personas from the old
               * Individuals so that the Individuals themselves are removed. */
1072
              foreach (var individual in candidate_inds)
1073
                {
1074
                  final_personas.add_all (individual.personas);
1075
                }
1076
            }
1077
          else if (candidate_inds.size > 0)
1078
1079
1080
            {
              debug ("    Linking disabled.");
            }
1081
1082
1083
1084
1085
1086
          else
            {
              debug ("    Did not find any candidate individuals.");
            }

          /* Create the final linked Individual */
Philip Withnall's avatar
Philip Withnall committed
1087
          var final_individual = new Individual (final_personas);
1088
1089
          debug ("    Created new individual '%s' (%p) with personas:",
              final_individual.id, final_individual);
1090
          foreach (var p in final_personas)