diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/AccountPersonaSearcherData.java b/Core/src/org/sleuthkit/autopsy/contentviewers/AccountPersonaSearcherData.java new file mode 100644 index 0000000000..4b8092adaf --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/AccountPersonaSearcherData.java @@ -0,0 +1,76 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers; + +import javax.swing.JButton; +import javax.swing.JLabel; + +/** + * A data bag for the persona searching thread. It wraps the account id to + * search for, and the UI label and button to update once the search completes. + */ +class AccountPersonaSearcherData { + + // Account identifier to search personas for. + private final String accountIdentifer; + // Persona name label to be updated when the search is complete. + private final JLabel personaNameLabel; + // Persona action button to be updated when the search is complete + private final JButton personaActionButton; + + /** + * Constructor. + * + * @param accountIdentifer Account identifier. + * @param personaNameLabel Persona name label. + * @param personaActionButton Persona button. + */ + AccountPersonaSearcherData(String accountIdentifer, JLabel personaNameLabel, JButton personaActionButton) { + this.accountIdentifer = accountIdentifer; + this.personaNameLabel = personaNameLabel; + this.personaActionButton = personaActionButton; + } + + /** + * Gets the account identifier. + * + * @return Account identifier. + */ + public String getAccountIdentifer() { + return accountIdentifer; + } + + /** + * Gets the persona name label. + * + * @return Persona name label. + */ + public JLabel getPersonaNameLabel() { + return personaNameLabel; + } + + /** + * Gets the persona button. + * + * @return Persona button. + */ + public JButton getPersonaActionButton() { + return personaActionButton; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogArtifactViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogArtifactViewer.java index e4c66a16be..1fbca12628 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogArtifactViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogArtifactViewer.java @@ -68,7 +68,7 @@ import org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsMode; * * */ -@ServiceProvider(service = ArtifactContentViewer.class) +//@ServiceProvider(service = ArtifactContentViewer.class) @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives public class CallLogArtifactViewer extends javax.swing.JPanel implements ArtifactContentViewer { diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogArtifactViewerNew.form b/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogArtifactViewerNew.form new file mode 100644 index 0000000000..9a459e56ae --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogArtifactViewerNew.form @@ -0,0 +1,18 @@ + + +
+ + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogArtifactViewerNew.java b/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogArtifactViewerNew.java new file mode 100644 index 0000000000..acc317e7d8 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogArtifactViewerNew.java @@ -0,0 +1,435 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers; + +import java.awt.Component; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Level; +import javax.swing.JScrollPane; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Artifact viewer for Call log artifacts. + * + * Displays the To/From and other parties, and metadata for a call. + */ +@ServiceProvider(service = ArtifactContentViewer.class) +public class CallLogArtifactViewerNew extends javax.swing.JPanel implements ArtifactContentViewer { + + private final static Logger logger = Logger.getLogger(CallLogArtifactViewerNew.class.getName()); + private static final long serialVersionUID = 1L; + + private static final Set HANDLED_ATTRIBUTE_TYPES = new HashSet(Arrays.asList( + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ID.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DIRECTION.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_END.getTypeID() + )); + + private GridBagLayout m_gridBagLayout = new GridBagLayout(); + private GridBagConstraints m_constraints = new GridBagConstraints(); + + private final List personaSearchtasks = new ArrayList<>(); + + /** + * Creates new form CallLogArtifactViewerNew + */ + public CallLogArtifactViewerNew() { + initComponents(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + setLayout(new java.awt.GridBagLayout()); + }// //GEN-END:initComponents + + @Override + public void setArtifact(BlackboardArtifact artifact) { + resetComponent(); + + CallLogViewDataNew callLogViewData = null; + try { + callLogViewData = getCallLogViewData(artifact); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, String.format("Error getting attributes for Calllog artifact (artifact_id=%d, obj_id=%d)", artifact.getArtifactID(), artifact.getObjectID()), ex); + } + + // update the view with the call log data + if (callLogViewData != null) { + updateView(callLogViewData); + } + // repaint + this.revalidate(); + + } + + /** + * Extracts data from the call log artifact for display in the view. + * + * @param artifact Artifact to extract data from. + * + * @return CallLogViewData Extracted data to be displayed. + * + * @throws TskCoreException + */ + private CallLogViewDataNew getCallLogViewData(BlackboardArtifact artifact) throws TskCoreException { + + if (artifact == null) { + return null; + } + + BlackboardAttribute directionAttr = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DIRECTION)); + BlackboardAttribute toAccountAttr = null; + BlackboardAttribute fromAccountAttr = null; + BlackboardAttribute localAccountAttr = null; + + CallLogViewDataNew callLogViewData = null; + + String direction = null; + String fromAccountIdentifier = null; + String toAccountIdentifier = null; + List otherParties = null; + + Content dataSource = artifact.getDataSource(); + String deviceId = ((DataSource) dataSource).getDeviceId(); + + if (directionAttr != null) { + direction = directionAttr.getValueString(); + if (direction.equalsIgnoreCase("Incoming")) { + fromAccountAttr = ObjectUtils.firstNonNull( + artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM)), + artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER)), + artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ID)) + ); + + toAccountAttr = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO)); + localAccountAttr = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO)); + } else if (direction.equalsIgnoreCase("Outgoing")) { + toAccountAttr = ObjectUtils.firstNonNull( + artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO)), + artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER)), + artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ID)) + ); + + fromAccountAttr = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM)); + localAccountAttr = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM)); + } + } + + // if direction isn't known, check all the usual attributes that may have the number/address + // in the absence of sufficent data, any number availabel will be displayed as a From address. + if (fromAccountAttr == null) { + fromAccountAttr = ObjectUtils.firstNonNull( + artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM)), + artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO)), + artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER)), + artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ID)) + ); + } + + if (fromAccountAttr != null) { + String fromAccountAttrValue = fromAccountAttr.getValueString(); + if (fromAccountAttrValue.equalsIgnoreCase(deviceId) == false) { + fromAccountIdentifier = fromAccountAttrValue; + } + } + + if (toAccountAttr != null) { + // TO may be a list of comma separated values. + String[] numbers = toAccountAttr.getValueString().split(","); + String toAccountAttrValue = StringUtils.trim(numbers[0]); + if (toAccountAttrValue.equalsIgnoreCase(deviceId) == false) { + toAccountIdentifier = toAccountAttrValue; + } + + // if more than one To address, then stcik the rest of them in the + // "Other parties" list. + if (numbers.length > 1) { + otherParties = new ArrayList<>(); + for (int i = 1; i < numbers.length; i++) { + otherParties.add(StringUtils.trim(numbers[i])); + } + } + } + + // if we have at least one address attribute + if (null != fromAccountAttr || null != toAccountAttr) { + callLogViewData = new CallLogViewDataNew(fromAccountIdentifier, toAccountIdentifier); + callLogViewData.setDirection(direction); + + callLogViewData.setOtherParties(otherParties); + + extractTimeAndDuration(artifact, callLogViewData); + + callLogViewData.setDataSourceName(dataSource.getName()); + + // set local account, if it can be deduced. + if (localAccountAttr != null) { + String attrValue = localAccountAttr.getValueString(); + // value must be a singular address and not a deviceId to be the local account id + if (attrValue.equalsIgnoreCase(deviceId) == false && attrValue.contains(",") == false) { + callLogViewData.setLocalAccountId(attrValue); + } + } + + callLogViewData.setOtherAttributes(extractOtherAttributes(artifact)); + } + + return callLogViewData; + } + + /** + * Extract the call time and duration from the artifact and saves in the + * CallLogViewData. + * + * @param artifact Call log artifact. + * @param callLogViewData CallLogViewData object to save the time & duration + * in. + * + * @throws TskCoreException + */ + private void extractTimeAndDuration(BlackboardArtifact artifact, CallLogViewDataNew callLogViewData) throws TskCoreException { + + BlackboardAttribute startTimeAttr = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START)); + if (startTimeAttr == null) { + startTimeAttr = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME)); + } + if (startTimeAttr != null) { + long startTime = startTimeAttr.getValueLong(); + callLogViewData.setDateTimeStr(startTimeAttr.getDisplayString()); + + BlackboardAttribute endTimeAttr = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_END)); + if (endTimeAttr != null) { + long endTime = endTimeAttr.getValueLong(); + if (endTime > 0 && (endTime - startTime) > 0) { + callLogViewData.setDuration(String.format("%d seconds", (endTime - startTime))); + } + } + } + } + + /** + * Returns the attributes from the given artifact that are not already + * displayed by the artifact viewer. + * + * @param artifact Call log artifact. + * + * @return Attribute names/values. + * + * @throws TskCoreException + */ + private Map extractOtherAttributes(BlackboardArtifact artifact) throws TskCoreException { + List attributes = artifact.getAttributes(); + Map otherAttributes = new HashMap<>(); + + for (BlackboardAttribute attr : attributes) { + if (HANDLED_ATTRIBUTE_TYPES.contains(attr.getAttributeType().getTypeID()) == false) { + otherAttributes.put(attr.getAttributeType().getDisplayName(), attr.getDisplayString()); + } + } + + return otherAttributes; + } + + /** + * Update the viewer with the call log data. + * + * @param callLogViewData Call log data to update the view with. + */ + private void updateView(CallLogViewDataNew callLogViewData) { + + CommunicationArtifactViewerHelper.addHeader(this, m_gridBagLayout, this.m_constraints, "Parties"); + + // Display From address + CommunicationArtifactViewerHelper.addKey(this, m_gridBagLayout, this.m_constraints, "From"); + + if (callLogViewData.getFromAccount() != null) { + // check if this is local account + String accountDisplayString = getAccountDisplayString(callLogViewData.getFromAccount(), callLogViewData); + CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, accountDisplayString); + + // show persona + Optional task = CommunicationArtifactViewerHelper.addPersonaRow(this, m_gridBagLayout, this.m_constraints, callLogViewData.getFromAccount()); + if (task.isPresent()) { + personaSearchtasks.add(task.get()); + } + } else { + CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, "Unknown"); + } + + // Display To: + CommunicationArtifactViewerHelper.addKey(this, m_gridBagLayout, this.m_constraints, "To"); + if (callLogViewData.getToAccount() != null) { + String accountDisplayString = getAccountDisplayString(callLogViewData.getToAccount(), callLogViewData); + CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, accountDisplayString); + + Optional task = CommunicationArtifactViewerHelper.addPersonaRow(this, m_gridBagLayout, this.m_constraints, callLogViewData.getToAccount()); + if (task.isPresent()) { + personaSearchtasks.add(task.get()); + } + } else { + CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, "Unknown"); + } + + // Display other parties + for (String otherParty : callLogViewData.getOtherParties()) { + CommunicationArtifactViewerHelper.addKey(this, m_gridBagLayout, this.m_constraints, "To"); + CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, otherParty); + + Optional task = CommunicationArtifactViewerHelper.addPersonaRow(this, m_gridBagLayout, this.m_constraints, otherParty); + if (task.isPresent()) { + personaSearchtasks.add(task.get()); + } + } + + updateMetadataView(callLogViewData); + updateSourceView(callLogViewData); + + CommunicationArtifactViewerHelper.addPageEndGlue(this, m_gridBagLayout, this.m_constraints); + + this.setLayout(m_gridBagLayout); + this.revalidate(); + this.repaint(); + } + + /** + * Update the call log meta data section. + * + * @param callLogViewData Call log data. + */ + private void updateMetadataView(CallLogViewDataNew callLogViewData) { + + CommunicationArtifactViewerHelper.addHeader(this, m_gridBagLayout, this.m_constraints, "Metadata"); + + CommunicationArtifactViewerHelper.addKey(this, m_gridBagLayout, this.m_constraints, "Direction"); + if (callLogViewData.getDirection() != null) { + CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, callLogViewData.getDirection()); + } else { + CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, "Unknown"); + } + + if (callLogViewData.getDateTimeStr() != null) { + CommunicationArtifactViewerHelper.addKey(this, m_gridBagLayout, this.m_constraints, "Date"); + CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, callLogViewData.getDateTimeStr()); + } + + if (callLogViewData.getDuration() != null) { + CommunicationArtifactViewerHelper.addKey(this, m_gridBagLayout, this.m_constraints, "Duration"); + CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, callLogViewData.getDuration()); + } + + } + + /** + * Update the call log source section. + * + * @param callLogViewData + */ + private void updateSourceView(CallLogViewDataNew callLogViewData) { + CommunicationArtifactViewerHelper.addHeader(this, m_gridBagLayout, this.m_constraints, "Source"); + CommunicationArtifactViewerHelper.addKey(this, m_gridBagLayout, this.m_constraints, "Data Source"); + CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, callLogViewData.getDataSourceName()); + } + + /** + * Returns display string for a account. + * Checks if the given account is the local account, if it is known. + * If it is, it appends a "(Local)" suffix to account display string. + * + * @param accountIdentifier Account identifier to check. + * @param callLogViewDataNew Call log data which may have the lock account. + * + * @return Account string to display. + */ + private String getAccountDisplayString(String accountIdentifier, CallLogViewDataNew callLogViewDataNew) { + String accountDisplayValue = accountIdentifier; + if (callLogViewDataNew.getLocalAccountId() != null && callLogViewDataNew.getLocalAccountId().equalsIgnoreCase(accountIdentifier)) { + accountDisplayValue += " (Local)"; + } + return accountDisplayValue; + } + + @Override + public Component getComponent() { + return new JScrollPane(this, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + + @Override + public boolean isSupported(BlackboardArtifact artifact) { + return artifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG.getTypeID(); + } + + /** + * Resets all artifact specific state. + */ + private void resetComponent() { + + // cancel any outstanding persona searching threads. + personaSearchtasks.forEach(task -> task.cancel(Boolean.TRUE)); + personaSearchtasks.clear(); + + // clear the panel + this.removeAll(); + this.setLayout(null); + + m_gridBagLayout = new GridBagLayout(); + m_constraints = new GridBagConstraints(); + + m_constraints.anchor = GridBagConstraints.FIRST_LINE_START; + m_constraints.gridy = 0; + m_constraints.gridx = 0; + m_constraints.weighty = 0.05; + m_constraints.weightx = 0.05; + m_constraints.insets = new java.awt.Insets(0, 0, 0, 0); + m_constraints.fill = GridBagConstraints.NONE; + + } + + + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogViewDataNew.java b/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogViewDataNew.java new file mode 100644 index 0000000000..065b456ccd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/CallLogViewDataNew.java @@ -0,0 +1,147 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Encapsulates the information to be displayed about a call log artifact. + */ +final class CallLogViewDataNew { + + private String fromAccount = null; + private String toAccount = null; + + // account identifier of the device owner, if known. + // will be one of the to or from account. + private String localAccountId = null; + + private String direction; + private String dateTimeStr = null; + private String duration = null; + + // Account identifers of other parties in the call. + private Collection otherParties = new ArrayList<>(); + + private Map otherAttributes = new HashMap<>(); + + private String dataSourceName = null; + + /** + * Constructor. + * @param fromAccount From account identifier, may be null; + * @param toAccount To account identifier, may be null; + */ + CallLogViewDataNew(String fromAccount, String toAccount) { + this(fromAccount, toAccount, null); + } + + /** + * Constructor. + * @param fromAccount From account identifier, may be null; + * @param toAccount To account identifier, may be null; + * @param direction Direction, may be null. + */ + CallLogViewDataNew(String fromAccount, String toAccount, String direction) { + this.fromAccount = fromAccount; + this.toAccount = toAccount; + this.direction = direction; + } + + String getFromAccount() { + return fromAccount; + } + + void setFromAccount(String fromAccount) { + this.fromAccount = fromAccount; + } + + String getToAccount() { + return toAccount; + } + + void setToAccount(String toAccount) { + this.toAccount = toAccount; + } + + String getDirection() { + return direction; + } + + void setDirection(String direction) { + this.direction = direction; + } + + String getDataSourceName() { + return dataSourceName; + } + + void setDataSourceName(String dataSourceName) { + this.dataSourceName = dataSourceName; + } + + String getDateTimeStr() { + return dateTimeStr; + } + + void setDateTimeStr(String dateTimeStr) { + this.dateTimeStr = dateTimeStr; + } + + String getDuration() { + return duration; + } + + void setDuration(String duration) { + this.duration = duration; + } + + Collection getOtherParties() { + return Collections.unmodifiableCollection(otherParties); + } + + void setOtherParties(Collection otherParticipants) { + if (otherParticipants != null) { + this.otherParties = new ArrayList<>(otherParticipants); + } + } + + public Map getOtherAttributes() { + return Collections.unmodifiableMap(otherAttributes); + } + + public void setOtherAttributes(Map otherAttributes) { + if (otherAttributes != null) { + this.otherAttributes = new HashMap<>(otherAttributes); + } + } + + public String getLocalAccountId() { + return localAccountId; + } + + public void setLocalAccountId(String localAccountId) { + this.localAccountId = localAccountId; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/CommunicationArtifactViewerHelper.java b/Core/src/org/sleuthkit/autopsy/contentviewers/CommunicationArtifactViewerHelper.java new file mode 100644 index 0000000000..08609aca66 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/CommunicationArtifactViewerHelper.java @@ -0,0 +1,334 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers; + +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.SwingUtilities; +import java.util.Optional; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; + +/** + * + * A class to help display a communication artifact in a panel using a + * gridbaglayout. + */ +public class CommunicationArtifactViewerHelper { + + // Number of columns in the gridbag layout. + private final static int MAX_COLS = 4; + + private final static int LEFT_INDENT = 12; + /** + * Adds a new heading to the panel. + * + * @param panel Panel to update. + * @param gridbagLayout Layout to use. + * @param constraints Constrains to use. + * @param headerString Heading string to display. + */ + static void addHeader(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String headerString) { + + // add a blank line before the start of new section, unless it's + // the first section + if (constraints.gridy != 0) { + addBlankLine(panel, gridbagLayout, constraints); + } + constraints.gridy++; + constraints.gridx = 0; + + // let the header span all of the row + constraints.gridwidth = MAX_COLS; + + // create label for heading + javax.swing.JLabel headingLabel = new javax.swing.JLabel(); + headingLabel.setText(headerString); + + // make it large and bold + headingLabel.setFont(headingLabel.getFont().deriveFont(Font.BOLD, headingLabel.getFont().getSize() + 2)); + + // add to panel + gridbagLayout.setConstraints(headingLabel, constraints); + panel.add(headingLabel); + + // reset constraints to normal + constraints.gridwidth = 1; + + // add line end glue + addLineEndGlue(panel, gridbagLayout, constraints); + } + + /** + * Adds a filler/glue at the end of the line to keep the other columns + * aligned, in case the panel is resized. + * + * @param panel Panel to update. + * @param gridbagLayout Layout to use. + * @param constraints Constrains to use. + */ + private static void addLineEndGlue(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints) { + // Place the filler just past the last column. + constraints.gridx = MAX_COLS; + + double savedWeightX = constraints.weightx; + int savedFill = constraints.fill; + + constraints.weightx = 1.0; // take up all the horizontal space + constraints.fill = GridBagConstraints.BOTH; + + javax.swing.Box.Filler horizontalFiller = new javax.swing.Box.Filler(new Dimension(0, 0), new Dimension(0, 0), new Dimension(32767, 0)); + gridbagLayout.setConstraints(horizontalFiller, constraints); + panel.add(horizontalFiller); + + // restore fill & weight + constraints.fill = savedFill; + constraints.weightx = savedWeightX; + } + + /** + * Adds a filler/glue at the bottom of the panel to keep the data rows + * aligned, in case the panel is resized. + * + * @param panel Panel to update. + * @param gridbagLayout Layout to use. + * @param constraints Constrains to use. + */ + static void addPageEndGlue(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints) { + + constraints.gridx = 0; + + double savedWeighty = constraints.weighty; + int savedFill = constraints.fill; + + constraints.weighty = 1.0; // take up all the vertical space + constraints.fill = GridBagConstraints.VERTICAL; + + javax.swing.Box.Filler vertFiller = new javax.swing.Box.Filler(new Dimension(0, 0), new Dimension(0, 0), new Dimension(0, 32767)); + gridbagLayout.setConstraints(vertFiller, constraints); + panel.add(vertFiller, constraints); + + //Resore weight & fill + constraints.weighty = savedWeighty; + constraints.fill = savedFill; + } + + /** + * Adds a blank line to the panel. + * + * @param panel Panel to update. + * @param gridbagLayout Layout to use. + * @param constraints Constrains to use. + */ + private static void addBlankLine(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints) { + constraints.gridy++; + constraints.gridx = 0; + + javax.swing.JLabel filler = new javax.swing.JLabel(" "); + gridbagLayout.setConstraints(filler, constraints); + panel.add(filler); + + addLineEndGlue(panel, gridbagLayout, constraints); + } + + /** + * Adds a label/key to the panel. + * + * @param panel Panel to update. + * @param gridbagLayout Layout to use. + * @param constraints Constrains to use. + * @param keyString Key name to display. + */ + static void addKey(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String keyString) { + + constraints.gridy++; + constraints.gridx = 0; + + Insets savedInsets = constraints.insets; + + // Set inset to indent in + constraints.insets = new java.awt.Insets(0, LEFT_INDENT, 0, 0); + + // create label, + javax.swing.JLabel keyLabel = new javax.swing.JLabel(); + keyLabel.setText(keyString + ": "); + + // add to panel + gridbagLayout.setConstraints(keyLabel, constraints); + panel.add(keyLabel); + + // restore inset + constraints.insets = savedInsets; + } + + /** + * Adds a value string to the panel. + * + * @param panel Panel to update. + * @param gridbagLayout Layout to use. + * @param constraints Constrains to use. + * @param keyString Value string to display. + */ + static void addValue(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String valueString) { + + constraints.gridx = 1; + + int savedGridwidth = constraints.gridwidth; + + // let the value span 2 cols + constraints.gridwidth = 2; + + // create label, + javax.swing.JLabel valueField = new javax.swing.JLabel(); + valueField.setText(valueString); + + // attach a right click menu with Copy option + valueField.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent evt) { + valueLabelMouseClicked(evt, valueField); + } + }); + + // add label to panel + gridbagLayout.setConstraints(valueField, constraints); + panel.add(valueField); + + // restore constraints + constraints.gridwidth = savedGridwidth; + + // end the line + addLineEndGlue(panel, gridbagLayout, constraints); + } + + /** + * Adds a Persona row to the panel. + * + * Adds a persona name label and a button to the panel. Kicks off a + * background task to search for persona for the given account. Updates the + * persona name and button when the task is done. + * + * If CentralRepostory is disabled, just displays 'Unknown' persona name. + * + * @param panel Panel to update. + * @param gridbagLayout Layout to use. + * @param constraints Constrains to use. + * @param accountIdentifier Account identifier to search the persona. + * + * @return Optional PersonaSearchAndDisplayTask started to search for + * persona. + */ + @NbBundle.Messages({ + "CommunicationArtifactViewerHelper_persona_label=Persona: ", + "CommunicationArtifactViewerHelper_persona_searching=Searching...", + "CommunicationArtifactViewerHelper_persona_unknown=Unknown", + "CommunicationArtifactViewerHelper_persona_button_view=View", + "CommunicationArtifactViewerHelper_persona_button_create=Create" + }) + static Optional addPersonaRow(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String accountIdentifier) { + + PersonaSearchAndDisplayTask personaTask = null; + + constraints.gridy++; + constraints.gridx = 1; + + Insets savedInsets = constraints.insets; + + // Indent in + constraints.insets = new java.awt.Insets(0, LEFT_INDENT, 0, 0); + + // create label + javax.swing.JLabel personaLabel = new javax.swing.JLabel(); + String personaLabelText = Bundle.CommunicationArtifactViewerHelper_persona_label(); + personaLabelText = personaLabelText.concat(CentralRepository.isEnabled() + ? Bundle.CommunicationArtifactViewerHelper_persona_searching() + : Bundle.CommunicationArtifactViewerHelper_persona_unknown()); + + personaLabel.setText(personaLabelText); + + // add to panel + gridbagLayout.setConstraints(personaLabel, constraints); + panel.add(personaLabel); + + // restore constraint + constraints.insets = savedInsets; + + constraints.gridx++; + + // Place a button as place holder. It will be enabled when persona is available. + javax.swing.JButton personaButton = new javax.swing.JButton(); + personaButton.setText(Bundle.CommunicationArtifactViewerHelper_persona_button_view()); + personaButton.setEnabled(false); + + + gridbagLayout.setConstraints(personaButton, constraints); + panel.add(personaButton); + + if (CentralRepository.isEnabled()) { + // kick off a task to find the persona for this account + personaTask = new PersonaSearchAndDisplayTask(panel, new AccountPersonaSearcherData(accountIdentifier, personaLabel, personaButton)); + personaTask.execute(); + } else { + personaLabel.setEnabled(false); + } + + addLineEndGlue(panel, gridbagLayout, constraints); + + return Optional.ofNullable(personaTask); + } + + /** + * Event handler for mouse click event. Attaches a 'Copy' menu item to right + * click. + * + * @param evt Event to check. + * @param valueLabel Label to attach the menu item to. + */ + @NbBundle.Messages({ + "CommunicationArtifactViewerHelper_menuitem_copy=Copy" + }) + private static void valueLabelMouseClicked(java.awt.event.MouseEvent evt, JLabel valueLabel) { + if (SwingUtilities.isRightMouseButton(evt)) { + JPopupMenu popup = new JPopupMenu(); + + JMenuItem copyMenu = new JMenuItem(Bundle.CommunicationArtifactViewerHelper_menuitem_copy()); // NON-NLS + copyMenu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(valueLabel.getText()), null); + + } + }); + + popup.add(copyMenu); + popup.show(valueLabel, evt.getX(), evt.getY()); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/PersonaSearchAndDisplayTask.java b/Core/src/org/sleuthkit/autopsy/contentviewers/PersonaSearchAndDisplayTask.java new file mode 100644 index 0000000000..f7fe876e7e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/PersonaSearchAndDisplayTask.java @@ -0,0 +1,220 @@ +/** + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers; + +import java.awt.Component; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.swing.JButton; +import javax.swing.SwingWorker; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoAccount; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.centralrepository.datamodel.Persona; +import org.sleuthkit.autopsy.centralrepository.datamodel.PersonaAccount; +import org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsDialog; +import org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsDialogCallback; +import org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsMode; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * Background task to search for a persona for a given account. + * + * When the search is complete, it updates the UI components + * for the persona appropriately. + * + */ + + @NbBundle.Messages({ + "# {0} - Persona count", + "PersonaDisplayTask_persona_count_suffix=(1 of {0})" + }) +class PersonaSearchAndDisplayTask extends SwingWorker, Void> { + + private final static Logger logger = Logger.getLogger(PersonaSearchAndDisplayTask.class.getName()); + + private final Component parentComponent; + private final AccountPersonaSearcherData personaSearcherData; + + PersonaSearchAndDisplayTask(Component parentComponent, AccountPersonaSearcherData personaSearcherData) { + this.parentComponent = parentComponent; + this.personaSearcherData = personaSearcherData; + } + + @Override + protected Collection doInBackground() throws Exception { + + Collection personas = new ArrayList<>(); + + if (CentralRepository.isEnabled()) { + Collection accountCandidates + = CentralRepoAccount.getAccountsWithIdentifier(personaSearcherData.getAccountIdentifer()); + + if (accountCandidates.isEmpty() == false) { + CentralRepoAccount account = accountCandidates.iterator().next(); + + // get personas for the account + Collection personaAccountsList = PersonaAccount.getPersonaAccountsForAccount(account.getId()); + personas = personaAccountsList.stream().map(PersonaAccount::getPersona) + .collect(Collectors.toList()); + } + } + return personas; + } + + @Override + protected void done() { + Collection personas; + try { + personas = super.get(); + + if (this.isCancelled()) { + return; + } + + //Update the Persona label and button based on the search result + String personaLabelText = Bundle.CommunicationArtifactViewerHelper_persona_label(); + String personaButtonText; + ActionListener buttonActionListener; + + if (personas.isEmpty()) { + // No persona found + personaLabelText += Bundle.CommunicationArtifactViewerHelper_persona_unknown(); + + // show a 'Create' button + personaButtonText = Bundle.CommunicationArtifactViewerHelper_persona_button_create(); + buttonActionListener = new CreatePersonaButtonListener(parentComponent, personaSearcherData); + } else { + Persona persona = personas.iterator().next(); + personaLabelText += persona.getName(); + if (personas.size() > 1) { + personaLabelText += Bundle.PersonaDisplayTask_persona_count_suffix(Integer.toString(personas.size())); + } + // Show a 'View' button + personaButtonText = Bundle.CommunicationArtifactViewerHelper_persona_button_view(); + buttonActionListener = new ViewPersonaButtonListener(parentComponent, persona); + } + + personaSearcherData.getPersonaNameLabel().setText(personaLabelText); + personaSearcherData.getPersonaActionButton().setText(personaButtonText); + personaSearcherData.getPersonaActionButton().setEnabled(true); + + // set button action + personaSearcherData.getPersonaActionButton().addActionListener(buttonActionListener); + } catch (CancellationException ex) { + logger.log(Level.INFO, "Persona searching was canceled."); //NON-NLS + } catch (InterruptedException ex) { + logger.log(Level.INFO, "Persona searching was interrupted."); //NON-NLS + } catch (ExecutionException ex) { + logger.log(Level.SEVERE, "Fatal error during Persona search.", ex); //NON-NLS + } + + } + + /** + * Action listener for Create persona button. + */ + private class CreatePersonaButtonListener implements ActionListener { + + private final Component parentComponent; + private final AccountPersonaSearcherData personaSearcherData; + + CreatePersonaButtonListener(Component parentComponent, AccountPersonaSearcherData personaSearcherData) { + this.parentComponent = parentComponent; + this.personaSearcherData = personaSearcherData; + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent evt) { + // Launch the Persona Create dialog + new PersonaDetailsDialog(parentComponent, + PersonaDetailsMode.CREATE, null, new PersonaCreateCallbackImpl(parentComponent, personaSearcherData)); + } + } + + /** + * Action listener for View persona button. + */ + private class ViewPersonaButtonListener implements ActionListener { + + private final Component parentComponent; + private final Persona persona; + + ViewPersonaButtonListener(Component parentComponent, Persona persona) { + this.parentComponent = parentComponent; + this.persona = persona; + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent evt) { + new PersonaDetailsDialog(parentComponent, + PersonaDetailsMode.VIEW, persona, new PersonaViewCallbackImpl()); + } + } + + /** + * Callback method for the create mode of the PersonaDetailsDialog + */ + class PersonaCreateCallbackImpl implements PersonaDetailsDialogCallback { + + private final Component parentComponent; + private final AccountPersonaSearcherData personaSearcherData; + + PersonaCreateCallbackImpl(Component parentComponent, AccountPersonaSearcherData personaSearcherData) { + this.parentComponent = parentComponent; + this.personaSearcherData = personaSearcherData; + } + + @Override + public void callback(Persona persona) { + JButton personaButton = personaSearcherData.getPersonaActionButton(); + if (persona != null) { + // update the persona name label with newly created persona, + // and change the button to a "View" button + personaSearcherData.getPersonaNameLabel().setText(Bundle.CommunicationArtifactViewerHelper_persona_label() + persona.getName()); + personaSearcherData.getPersonaActionButton().setText(Bundle.CallLogArtifactViewer_persona_button_view()); + + // replace action listener with a View button listener + for (ActionListener act : personaButton.getActionListeners()) { + personaButton.removeActionListener(act); + } + personaButton.addActionListener(new ViewPersonaButtonListener(parentComponent, persona)); + + } + + personaButton.getParent().revalidate(); + } + } + + /** + * Callback method for the view mode of the PersonaDetailsDialog + */ + class PersonaViewCallbackImpl implements PersonaDetailsDialogCallback { + + @Override + public void callback(Persona persona) { + // nothing to do + } + } +}