Merge changes I650bb201,I7b194b79,Icb452f10

* changes:
  Use method from AbstractDaemonTest to create branches
  AbstractCodeOwnersTest: Add method to enable the code owners functionality
  CheckCodeOwnerConfigFilesIT: Test that non-visible branches are omitted
diff --git a/ui/code-owners-service.js b/ui/code-owners-service.js
index 426d0a7..1d6f738 100644
--- a/ui/code-owners-service.js
+++ b/ui/code-owners-service.js
@@ -37,6 +37,19 @@
 };
 
 /**
+ * @enum
+ */
+const UserRole = {
+  ANONYMOUS: 'ANONYMOUS',
+  AUTHOR: 'AUTHOR',
+  CHANGE_OWNER: 'CHANGE_OWNER',
+  REVIEWER: 'REVIEWER',
+  CC: 'CC',
+  REMOVED_REVIEWER: 'REMOVED_REVIEWER',
+  OTHER: 'OTHER',
+}
+
+/**
  * Responsible for communicating with the rest-api
  *
  * @see resources/Documentation/rest-api.md
@@ -123,22 +136,90 @@
    * Initial fetches.
    */
   init() {
-    this.statusPromise = this.codeOwnerApi
-        .listOwnerStatus(this.change._number)
-        .then(res => {
-          return {
-            patchsetNumber: res.patch_set_number,
-            codeOwnerStatusMap: this._formatStatuses(
-                res.file_code_owner_statuses
-            ),
-            rawStatuses: res.file_code_owner_statuses,
-          };
-        });
+    this.accountPromise = this.restApi.getLoggedIn().then(loggedIn => {
+      if (!loggedIn) {
+        return undefined;
+      }
+      return this.restApi.getAccount();
+    });
+
+    this.statusPromise = this.isCodeOwnerEnabled().then(enabled => {
+      if (!enabled) {
+        return {
+          patchsetNumber: 0,
+          enabled: false,
+          codeOwnerStatusMap: new Map(),
+          rawStatuses: [],
+        };
+      }
+      return this.codeOwnerApi
+          .listOwnerStatus(this.change._number)
+          .then(res => {
+            return {
+              enabled: true,
+              patchsetNumber: res.patch_set_number,
+              codeOwnerStatusMap: this._formatStatuses(
+                  res.file_code_owner_statuses
+              ),
+              rawStatuses: res.file_code_owner_statuses,
+            };
+          });
+    });
+  }
+
+  /**
+   * Returns the role of the current user. The returned value reflects the
+   * role of the user at the time when the change is loaded.
+   * For example, if a user removes themselves as a reviewer, the returned
+   * role 'REVIEWER' remains unchanged until the change view is reloaded.
+   */
+  getLoggedInUserInitialRole() {
+    return this.accountPromise.then(account => {
+      if (!account) {
+        return UserRole.ANONYMOUS;
+      }
+      const change = this.change;
+      if (
+        change.revisions &&
+        change.current_revision &&
+        change.revisions[change.current_revision]
+      ) {
+        const commit = change.revisions[change.current_revision].commit;
+        if (
+          commit &&
+          commit.author &&
+          account.email &&
+          commit.author.email === account.email
+        ) {
+          return UserRole.AUTHOR;
+        }
+      }
+      if (change.owner._account_id === account._account_id) {
+        return UserRole.CHANGE_OWNER;
+      }
+      if (change.reviewers) {
+        if (this._accountInReviewers(change.reviewers.REVIEWER, account)) {
+          return UserRole.REVIEWER;
+        } else if (this._accountInReviewers(change.reviewers.CC, account)) {
+          return UserRole.CC;
+        } else if (this._accountInReviewers(change.reviewers.REMOVED, account)) {
+          return UserRole.REMOVED_REVIEWER;
+        }
+      }
+      return UserRole.OTHER;
+    })
+  }
+
+  _accountInReviewers(reviewers, account) {
+    if (!reviewers) {
+      return false;
+    }
+    return reviewers.some(reviewer => reviewer._account_id === account._account_id);
   }
 
   getStatus() {
     return this.statusPromise.then(res => {
-      if (!this.isOnLatestPatchset(res.patchsetNumber)) {
+      if (res.enabled && !this.isOnLatestPatchset(res.patchsetNumber)) {
         // status is outdated, abort and re-init
         this.abort();
         this.init();
@@ -325,15 +406,6 @@
     };
   }
 
-  /**
-   * Returns a promise with whether status is for latest patchset or not.
-   */
-  isStatusOnLatestPatchset() {
-    return this.statusPromise.then(({patch_set_id}) => {
-      return this.isOnLatestPatchset(patch_set_id);
-    });
-  }
-
   isOnLatestPatchset(patchsetId) {
     const latestRevision = this.change.revisions[this.change.current_revision];
     return `${latestRevision._number}` === `${patchsetId}`;
@@ -388,7 +460,11 @@
   }
 
   getProjectConfig() {
-    return this.codeOwnerApi.getProjectConfig(this.change.project);
+    if (!this.getProjectConfigPromise) {
+      this.getProjectConfigPromise =
+          this.codeOwnerApi.getProjectConfig(this.change.project);
+    }
+    return this.getProjectConfigPromise;
   }
 
   isCodeOwnerEnabled() {
@@ -414,4 +490,4 @@
     }
     return this.ownerService;
   }
-}
\ No newline at end of file
+}
diff --git a/ui/code-owners-service_test.html b/ui/code-owners-service_test.html
index f0678da..6986ea9 100644
--- a/ui/code-owners-service_test.html
+++ b/ui/code-owners-service_test.html
@@ -18,6 +18,9 @@
       get() {
         return Promise.resolve({});
       },
+      getLoggedIn() {
+        return Promise.resolve(undefined);
+      },
     };
     const getApiStub = sinon.stub(fakeRestApi, 'get');
     getApiStub.returns(Promise.resolve({}));
@@ -233,4 +236,4 @@
       });
     });
   });
-</script>
\ No newline at end of file
+</script>
diff --git a/ui/owner-requirement.js b/ui/owner-requirement.js
index e456976..610fdd4 100644
--- a/ui/owner-requirement.js
+++ b/ui/owner-requirement.js
@@ -123,13 +123,14 @@
     return this.ownerService.getStatus()
         .then(({rawStatuses}) => {
           this._statusCount = this._getStatusCount(rawStatuses);
-
-          // Send a metric with overall summary when code owners submit
-          // requirement shown and finished fetching status
-          this.reporting.reportLifeCycle(
-              'owners-submit-requirement-summary-shown',
-              {...this._statusCount}
-          );
+          this.ownerService.getLoggedInUserInitialRole().then(role => {
+            // Send a metric with overall summary when code owners submit
+            // requirement shown and finished fetching status
+            this.reporting.reportLifeCycle(
+                'owners-submit-requirement-summary-shown',
+                {...this._statusCount, user_role: role}
+            );
+          });
         })
         .finally(() => {
           this._isLoading = false;
@@ -205,9 +206,11 @@
         })
     );
     ownerState.expandSuggestion = true;
-
-    this.reporting.reportInteraction('suggest-owners-from-submit-requirement');
+    this.ownerService.getLoggedInUserInitialRole().then(role => {
+      this.reporting.reportInteraction(
+          'suggest-owners-from-submit-requirement', {user_role: role});
+    });
   }
 }
 
-customElements.define(OwnerRequirementValue.is, OwnerRequirementValue);
\ No newline at end of file
+customElements.define(OwnerRequirementValue.is, OwnerRequirementValue);
diff --git a/ui/owner-ui-state.js b/ui/owner-ui-state.js
index 9d82771..3d47ef9 100644
--- a/ui/owner-ui-state.js
+++ b/ui/owner-ui-state.js
@@ -14,6 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/**
+ * @enum
+ */
+const OwnerUIStateEventType = {
+  EXPAND_SUGGESTION: 'expandSuggestion',
+};
+
 /**
  * For states used in code owners plugin across multiple components.
  */
@@ -21,7 +29,7 @@
   constructor() {
     this._expandSuggestion = false;
     this._listeners = new Map();
-    this._listeners.set('expandSuggestion', []);
+    this._listeners.set(OwnerUIStateEventType.EXPAND_SUGGESTION, []);
   }
 
   get expandSuggestion() {
@@ -30,7 +38,7 @@
 
   set expandSuggestion(value) {
     this._expandSuggestion = value;
-    this._listeners.get('expandSuggestion').forEach(cb => {
+    this._listeners.get(OwnerUIStateEventType.EXPAND_SUGGESTION).forEach(cb => {
       try {
         cb(value);
       } catch (e) {
@@ -39,9 +47,23 @@
     });
   }
 
+  _subscribeEvent(eventType, cb) {
+    this._listeners.get(eventType).push(cb);
+    return () => {
+      this._unsubscribeEvent(eventType, cb);
+    };
+  }
+
+  _unsubscribeEvent(eventType, cb) {
+    this._listeners.set(
+        eventType,
+        this._listeners.get(eventType).filter(handler => handler !== cb)
+    );
+  }
+
   onExpandSuggestionChange(cb) {
-    this._listeners.get('expandSuggestion').push(cb);
+    return this._subscribeEvent(OwnerUIStateEventType.EXPAND_SUGGESTION, cb);
   }
 }
 
-export const ownerState = new OwnerUIState();
\ No newline at end of file
+export const ownerState = new OwnerUIState();
diff --git a/ui/suggest-owners-trigger.js b/ui/suggest-owners-trigger.js
index 5745545..2c0ccc0 100644
--- a/ui/suggest-owners-trigger.js
+++ b/ui/suggest-owners-trigger.js
@@ -22,6 +22,11 @@
     return 'suggest-owners-trigger';
   }
 
+  constructor(props) {
+    super(props);
+    this.expandSuggestionStateUnsubscriber = undefined;
+  }
+
   static get properties() {
     return {
       change: Object,
@@ -78,9 +83,18 @@
 
   connectedCallback() {
     super.connectedCallback();
-    ownerState.onExpandSuggestionChange(expanded => {
-      this.expanded = expanded;
-    });
+    this.expandSuggestionStateUnsubscriber = ownerState
+        .onExpandSuggestionChange(expanded => {
+          this.expanded = expanded;
+        });
+  }
+
+  disconnnectedCallback() {
+    super.disconnectedCallback();
+    if (this.expandSuggestionStateUnsubscriber) {
+      this.expandSuggestionStateUnsubscriber();
+      this.expandSuggestionStateUnsubscriber = undefined;
+    }
   }
 
   onInputChanged(restApi, change) {
@@ -90,13 +104,18 @@
         this.change
     );
 
-    Promise.all([this.ownerService.isCodeOwnerEnabled(), this.ownerService.areAllFilesApproved()])
-        .then(([enabled, approved]) => {
+    Promise.all([
+      this.ownerService.isCodeOwnerEnabled(),
+      this.ownerService.areAllFilesApproved(),
+      this.ownerService.getLoggedInUserInitialRole()
+    ])
+        .then(([enabled, approved, userRole]) => {
           if (enabled) {
             this.hidden = approved;
           } else {
             this.hidden = true;
           }
+          this._userRole = userRole;
         });
   }
 
@@ -105,6 +124,7 @@
     ownerState.expandSuggestion = this.expanded;
     this.reporting.reportInteraction('toggle-suggest-owners', {
       expanded: this.expanded,
+      user_role: this._userRole ? this._userRole : 'UNKNOWN',
     });
   }
 
@@ -121,4 +141,4 @@
   }
 }
 
-customElements.define(SuggestOwnersTrigger.is, SuggestOwnersTrigger);
\ No newline at end of file
+customElements.define(SuggestOwnersTrigger.is, SuggestOwnersTrigger);
diff --git a/ui/suggest-owners.js b/ui/suggest-owners.js
index 9919e54..2e1a487 100644
--- a/ui/suggest-owners.js
+++ b/ui/suggest-owners.js
@@ -24,6 +24,11 @@
     return 'owner-group-file-list';
   }
 
+  constructor() {
+    super();
+    this.expandSuggestionStateUnsubscriber = undefined;
+  }
+
   static get properties() {
     return {
       files: Array,
@@ -327,30 +332,40 @@
 
   connectedCallback() {
     super.connectedCallback();
-    ownerState.onExpandSuggestionChange(expanded => {
-      this.hidden = !expanded;
-      if (expanded) {
-        // this is more of a hack to let reivew input lose focus
-        // to avoid suggestion dropdown
-        // gr-autocomplete has a internal state for tracking focus
-        // that will be canceled if any click happens outside of
-        // it's target
-        // Can not use `this.async` as it's only available in
-        // legacy element mixin which not used in this plugin.
-        Polymer.Async.timeOut.run(() => this.click(), 100);
+    this.expandSuggestionStateUnsubscriber = ownerState
+        .onExpandSuggestionChange(expanded => {
+          this.hidden = !expanded;
+          if (expanded) {
+            // this is more of a hack to let reivew input lose focus
+            // to avoid suggestion dropdown
+            // gr-autocomplete has a internal state for tracking focus
+            // that will be canceled if any click happens outside of
+            // it's target
+            // Can not use `this.async` as it's only available in
+            // legacy element mixin which not used in this plugin.
+            Polymer.Async.timeOut.run(() => this.click(), 100);
 
-        // start fetching suggestions
-        if (!this._suggestionsTimer) {
-          this._suggestionsTimer = setInterval(() => {
-            this._pollingSuggestions();
-          }, SUGGESTION_POLLING_INTERVAL);
+            // start fetching suggestions
+            if (!this._suggestionsTimer) {
+              this._suggestionsTimer = setInterval(() => {
+                this._pollingSuggestions();
+              }, SUGGESTION_POLLING_INTERVAL);
 
-          // poll immediately to kick start the fetching
-          this.reporting.reportLifeCycle('owners-suggestions-fetching-start');
-          this._pollingSuggestions();
-        }
-      }
-    });
+              // poll immediately to kick start the fetching
+              this.reporting
+                  .reportLifeCycle('owners-suggestions-fetching-start');
+              this._pollingSuggestions();
+            }
+          }
+        });
+  }
+
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    if (this.expandSuggestionStateUnsubscriber) {
+      this.expandSuggestionStateUnsubscriber();
+      this.expandSuggestionStateUnsubscriber = undefined;
+    }
   }
 
   onInputChanged(restApi, change) {