From f518a236681f2605dc050dafd884818abf0ddf57 Mon Sep 17 00:00:00 2001 From: Rafael Teixeira Date: Mon, 26 Oct 2015 21:24:45 -0200 Subject: [PATCH 01/51] Project organized and pt-br translation started --- app/src/main/res/values-pt/strings.xml | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 app/src/main/res/values-pt/strings.xml diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml new file mode 100644 index 00000000..b8ccbec8 --- /dev/null +++ b/app/src/main/res/values-pt/strings.xml @@ -0,0 +1,51 @@ + + + Buendia + Medicação e dosagem + %d m + %d a + %1$d a %2$d m + + Adicionar resultado de teste + Condição + Atribuir localização + Editar + Editar paciente + Pesquisar por ID + + Criar novo paciente + Novo turno + Pesquisar + Atualizar quadro + Arquivo de turnos + + Vizualisar primeiro xform salvo + Criar + Erro ao conectar com o servidor + Usuário inválido + Erro desconhecido + Usuário já existe no tablet + Nome de usuário já existe no servidor + Desde a admissão + Data de admissão + Definir data de admissão + Idade + Menos de %d anos + TODOS PACIENTES ATUAIS + Impossível conectar-se com o servidor. Por favor verifique a barra na base da tela para mais informações sobre o problema. + A + D + \? + I + V + Cancelar + Cancelar sincronização + Cancelando sincronização... + "Último quadro atualizado: " + Concluir e limpar dados locais + Sincronização completa! + Condição + Estado de consiência + Dia %d + Liberado + Requisição de tede interrompida. + O servidor rejeitou a nova observação. Motivo: %s + \ No newline at end of file From a11845f14053896a9c673262f5d1ec029ed0e822 Mon Sep 17 00:00:00 2001 From: Rafael Teixeira Date: Tue, 17 Nov 2015 21:36:11 -0200 Subject: [PATCH 02/51] =?UTF-8?q?Strings.xml=20traduzido=20(necessita=20re?= =?UTF-8?q?vis=C3=A3o)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values-pt/strings.xml | 223 ++++++++++++++++++++++++- 1 file changed, 222 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b8ccbec8..4b652db8 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -41,11 +41,232 @@ Cancelando sincronização... "Último quadro atualizado: " Concluir e limpar dados locais - Sincronização completa! + Completando sincronização... Condição Estado de consiência Dia %d Liberado Requisição de tede interrompida. O servidor rejeitou a nova observação. Motivo: %s + Nome de usuário ou senha inválidos + Observações foram atualizadas, dados locais podem ficar temporariamente fora de sincronia. + O servidor falhou em processar essa atualização. + Erro desconhecido + Erro desconhecido no servidor. + Ocorreu um erro desconhecio ao definir a localização do paciente. Por favor tente novamente. + Sobrenome + Sobrenome não pode ser nulo + Feminino + O formulário não foi encontrado no servidor. + Incapaz de acessar o servidor. Por favor verifique se seu nome de usuário e senha estão corretos. + Erro ao recuperar a lista de formulários do servidor. + Falha ao buscar o formulário no servior. + Erro ao recuperar o formulário do servidor. + Erro desconhecido. + Múltiplos filtros + Nome + Nome não pode ser nulo + Ir para quadro + ID do paciente + Nenhum dado carregado do servidor ainda + Paciente não encontrado no servidor + Nenhum paciente como %s encontrado. + Ir para quadro do paciente + ID + Nome de usuário deve ter entre 2 e 50 caracteresl. Apenas letras, dígitos ".", "-" e "_" são permitidos. + Catéter intravenoso aplicado + anos + Nenhuma observação + Teste de Ebola + Teste de Ebola (%s) + Carregando... + Localização + Erro ao carregar recursos de localização + Masculino + Leve + As + BB (?) + \? + C + Moderado + Cd + meses + %d pacientes + %d pacientes presentes + Nenhum paciente + Nenhum paciente presente + Nenhum paciente, como há 48 min + Observação + 1 paciente + 1 paciente presente + Dosagem + (parar depois de %s) + (parar depois de hoje) + (parar depois de amanhã) + (sem duração específica) + Dado <b>%1$d</b> vezes em %2$s + Dado <b>%1$d</b> vez em %2$s + Marcar como dado agora + ✔ Marcar como dado agora + Histórico de tratamento + Dado <b>%1$d</b> vezes hoje, %2$s + Dado <b>%1$d</b> vez hoje, %2$s + Frequência + Dar para + dia + dias + Deve ser vazio ou maior que 0 + Medicação + Madicação não pode ficar em branco + Começo em %1$s as %2$s + vez por dia + vezes por dia + Cancelar + Limpar + Impossível adicionar paciente no bando de dados local + Trabalhando… + Outro paciente existe com o mesmo ID + Incapaz de adicionar paciente: %s + Sobrenome + Nome + Requisição ao servidor interrompida + sobrenome inválido + nome inválido + ID do paciente inválido + Atribuir localização + Localização + falha ao completar a requisição ao servidor + falha ao completar a requisição ao servido (motivo: %s) + o servidor não retornou corretamente os dados do paciente + Novo paciente adicionado. + erro desconhecido + Erro ao sincronizar dados do paciente. Por favor verifique as configurações ou tente novamente. + A atualização da localização do paciente foi interrompida. Por favor tente novamente. + Erro ao atualizar o paciente; por favor verifique se você está conectado ao servidor. + Erro ao atualizar a localização do paciente: este paciente não existe no servidor. + Erro desconhecido ao atualizar a localização do paciente. Por favor tente novamente. + Insira a idade (use anos, meses ou os dois) + Por favor selecione Masculino ou Feminino. + Falha na sincronização do paciente + Data de admissão não pode ser no futuro. + Data de início do sintoma não pode ser no futuro. + Insira a data de admissão + Insira o sobrenome + Insita o nome + Insira o ID do paciente + NEG + Por favor aguarde… + Isto é mais rápido mas ainda é experimental. + Desative isso para permitir que o app funcione com redes não-wifi (emuladas ou via Bluetooth). + Normalmente os fomulários são apagados depois de enviados para o servidor. Selecione esta opção para mantê-los para debugging. + Isto fornece uma melhor experiência de UI, mas é um aspecto experimental. + Avançado + Desenvolvedor + Geral + Intervalo de verificação de atualizações de APK (segundos) + Sincronização de observações incremental + Senha OpenMRS + URL base do OpenMRS + Nome de usuário OpenMRS + URL do servidor de pacotes + Exigir conexão wifi + Servidor Buendia + Salvar instâncias de formulário localmente + Atualizar observações locais ao submetê-las + Grávida + Todos pacientes presentes + Por favor aguarde enquanto o formulário requerido é recuperado. + Recuperando formulário + Severo + Sexo + Desde a admissão + Desde o surgimento dos sintomas + Download + Instalar + Atualização do aplicativo disponível + Atualização do aplicativo baixada + Morte confirmada + Convalescendo + Crítico + Liberado curado + Paliativo + 6 + 5 + 3 + 7 + 8 + 4 + 6? + - + 2 + 1 + Suspeita de morte + Desconhecido + Indisposto + Bem + %1$s(%2$s) + Erro ao enviar formulário: falha na autenticação com o servidor. + Tempo limite excedido ao enviar formulário para o servidor. + Algo de errado aconteceu ao enviar o formulário. + Por favor aguarde enquando os dados do formulário são enviados. + Enviando formulário + Data de sugimento do sintoma + Data estimada do surgimento do sintoma + Voltar + Ocorreu um erro ao recuperar dados do servidor. Por favor verifique suas configurações OpenMRS ou tente novamente. + Erro de sincronização + Tentar novamente + Configurações + Iniciando sincronização... + Iniciando sincronização... + Syncronizando quadros... + Sincronizando conceitos + Sincronizando formulários... + Sincronizando localizações... + Sincronizando observações... + Sincronizando ordens... + Sincronizando pacientes... + Sincronizando usuários... + Por favor aguarde enquanto os dados do paciente e da localização são obtidos. Se essa for a primeira vez que os dados do paciente são carregados, esse processo pode levar alguns minutos. + Carregando... + Novo paciente + Configurações + Descartar alterações? + Editar localização do paciente + Localizações + Novo tratamento + Novo usuário + Quadro do paciente + Pacientes + Pacientes nessa localização + Atualizando paciente + Hoje + Hoje, %s + Verifique as configurações + Mais informações + Configuração do servidor de pacotes pode estar incorreta + Servidor de pacotes inacessível + Endereçço do servidor pode estar incorredo + Verificar + Nome de usuário ou senha do servidor podem estar incorretas + Verificar + Servidor não respondendo + O servido não está respondendo a requisições no momento. Isso pode ser por que: \n \n • O servidor está temporariamente indisponível. + Servidor inalcançável + O servidor não pôde ser alcançado. Isso pode ser por que: \n \n • A rede wifi está incorreta. \n • A URL do servidor está incorreta. \n • O servidor está fora do ar. \n \nPor favor entre em contato com um administrador. + O servidor pode estar instável + Os servidor no momentoe está respondendo com um erro de códgo 500, indicando que o servidor pode estar num estado de erro. \n \nPor favor entre em contat com um administrador. + Por favor verifique se as configurações do servidor de pacotes estão corretas ou entre em contato com um administrador para conseguir ajuda. + O servidor de pacotes não pode ser alcançado. Isso pode ser por que: \n \n • A URL do servidor está incorreta. \n • O servidor está fora do ar. \n \nPor favor corrija as confirgurações do servidor de pacotes ou entre em contato com um administrador para obter ajuda. + O wifi está desativado + Ativar + Wifi está disconectado + Conectar + Desconhecido + Localização desconhecida + Tenda desconhecida + %s (nenhuma tenda atribuída) + Zona desconhecida + Ocorreu um erro ao recuperar a lista de usuarios. Por favor verifique suas configurações OpenMRS ou tente novamente. + \"Nome de usuário não pode ser nulo\" \ No newline at end of file From 87e476c1ea51cc1bed774e3defb3891a9fe7ea7a Mon Sep 17 00:00:00 2001 From: Fabian Tamp Date: Tue, 19 Jan 2016 10:36:10 +0800 Subject: [PATCH 03/51] Enable multidex to temporarily avert the dexocalypse. - Bump build tools version to latest. This is required for Multidex. - Delete ic_launcher from OdkCollect, required because later versions of the AAPT build tool handle duplicate resources as errors, and this file was causing the build to fail. - Add dependency upon the legacy HTTP library. API 23 deprecated Apache HTTP, but we're still using it for Health check code. - Update OdkCollect to latest build tools as well, because it was separately including the legacy Apache HTTP library, which was resulting in conflicts from having the same JAR included twice. Updating to the latest build tools means that it can use the same legacy library as `:app`, which eliminates the conflict. - Update support library versions to 23 so that they work with the latest build tools. - Enable Multidex. Tested on API 21 (L) and 19 (KK) - Remove Guava as a direct dependency, and delete code that used it from `JsonPatient`. Note that Guava is still included by Pebble as a transitive dependency, but I don't want developers on this project to be able to use it in code we control, so we have the flexibility to remove the dependency one day. See https://slack-files.com/T02T5LNM4-F0JQ1UDRV-716ebe431f for details. --- app/build.gradle | 25 ++++++++++++++---- .../java/org/projectbuendia/client/App.java | 9 +++++++ .../client/json/JsonPatient.java | 15 ----------- build.gradle | 2 +- third_party/odkcollect/build.gradle | 14 ++++++---- .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 19388 -> 0 bytes 6 files changed, 39 insertions(+), 26 deletions(-) delete mode 100644 third_party/odkcollect/src/main/res/drawable-xxhdpi/ic_launcher.png diff --git a/app/build.gradle b/app/build.gradle index 86797d56..99163db3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,6 +32,11 @@ android { preDexLibraries = false javaMaxHeapSize = '4g' } + + // Enable multidex support. + defaultConfig { + multiDexEnabled true + } } dependencies { // Build plugins @@ -44,10 +49,9 @@ dependencies { compile project(':third_party:odkcollect') // External dependencies - compile 'com.android.support:appcompat-v7:22.2.0' - compile 'com.android.support:support-annotations:22.2.0' + compile 'com.android.support:appcompat-v7:23.1.1' + compile 'com.android.support:support-annotations:23.1.1' compile 'com.google.code.gson:gson:2.3' // JSON parser - compile 'com.google.guava:guava:18.0' // Google common libraries compile 'com.jakewharton:butterknife:5.1.2' // View injection compile 'com.mcxiaoke.volley:library:1.0.6' // HTTP framework compile 'com.joanzapata.android:android-iconify:1.0.8' // Font-based icons @@ -62,6 +66,9 @@ dependencies { // Testing androidTestCompile 'com.android.support.test:runner:0.3' + // Explicitly add this dep at 23.1.1, because the above entry depends on 22.2.0, and the + // discrepancy can introduce differences in behaviour between prod and test. + androidTestCompile 'com.android.support:support-annotations:23.1.1' // Espresso androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2' androidTestCompile 'com.android.support.test.espresso:espresso-web:2.2' @@ -71,6 +78,11 @@ dependencies { androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.0' androidTestCompile 'com.google.dexmaker:dexmaker:1.0' androidTestCompile 'org.mockito:mockito-core:1.9.5' + + // Multidex. + // NOTE: This is temporary only! See https://slack-files.com/T02T5LNM4-F0JQ1UDRV-716ebe431f + // for more information. + compile 'com.android.support:multidex:1.0.1' } apply plugin: 'spoon' @@ -170,8 +182,11 @@ logger.info("Default package server root URL: ${packageServerRootUrl}") logger.info("Database encryption password: ${encryptionPassword}") android { - compileSdkVersion 21 - buildToolsVersion '19.1.0' + compileSdkVersion 23 + buildToolsVersion '23.0.2' + // TODO: Port the various health checks to use HttpURLConnection instead and remove this + // dependency. + useLibrary 'org.apache.http.legacy' sourceSets.main { jniLibs.srcDir 'libs' diff --git a/app/src/main/java/org/projectbuendia/client/App.java b/app/src/main/java/org/projectbuendia/client/App.java index d4f1f61d..b84f7d96 100644 --- a/app/src/main/java/org/projectbuendia/client/App.java +++ b/app/src/main/java/org/projectbuendia/client/App.java @@ -12,7 +12,9 @@ package org.projectbuendia.client; import android.app.Application; +import android.content.Context; import android.preference.PreferenceManager; +import android.support.multidex.MultiDex; import com.facebook.stetho.Stetho; @@ -85,6 +87,13 @@ public static synchronized OpenMrsConnectionDetails getConnectionDetails() { mHealthMonitor.start(); } + @Override + public void attachBaseContext(Context base) { + // Set up Multidex. + super.attachBaseContext(base); + MultiDex.install(this); + } + public void inject(Object obj) { mObjectGraph.inject(obj); } diff --git a/app/src/main/java/org/projectbuendia/client/json/JsonPatient.java b/app/src/main/java/org/projectbuendia/client/json/JsonPatient.java index feb54edf..a36c932b 100644 --- a/app/src/main/java/org/projectbuendia/client/json/JsonPatient.java +++ b/app/src/main/java/org/projectbuendia/client/json/JsonPatient.java @@ -11,8 +11,6 @@ package org.projectbuendia.client.json; -import com.google.common.base.MoreObjects; - import org.joda.time.LocalDate; import java.io.Serializable; @@ -34,17 +32,4 @@ public class JsonPatient implements Serializable { public JsonPatient() { } - - @Override public String toString() { - return MoreObjects.toStringHelper(this) - .add("uuid", uuid) - .add("voided", voided) - .add("id", id) - .add("given_name", given_name) - .add("family_name", family_name) - .add("sex", sex) - .add("birthdate", birthdate.toString()) - .add("assigned_location", assigned_location) - .toString(); - } } diff --git a/build.gradle b/build.gradle index ab62e909..346dadb7 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:1.2.3' + classpath 'com.android.tools.build:gradle:1.5.0' } } diff --git a/third_party/odkcollect/build.gradle b/third_party/odkcollect/build.gradle index c63b3bbc..785f3b64 100644 --- a/third_party/odkcollect/build.gradle +++ b/third_party/odkcollect/build.gradle @@ -7,8 +7,13 @@ buildscript { apply plugin: 'com.android.library' android { - compileSdkVersion 21 - buildToolsVersion "19.1.0" + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + // Upon switching to API 23, we can use this to include the Apache HTTP jar instead of + // including it manually. + useLibrary 'org.apache.http.legacy' + defaultConfig { minSdkVersion 19 targetSdkVersion 21 @@ -32,8 +37,8 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:21.0.0' - compile 'com.android.support:support-v4:21.0.0' + compile 'com.android.support:appcompat-v7:23.1.1' + compile 'com.android.support:support-v4:23.1.1' compile 'com.google.code.gson:gson:2.3' compile 'commons-io:commons-io:2.4' compile 'joda-time:joda-time:2.3' @@ -43,6 +48,5 @@ dependencies { compile('org.apache.james:apache-mime4j:0.7.2') { exclude group: 'commons-logging', module: 'commons-logging' } - compile files('libs/httpclientandroidlib-4.2.1.jar') compile files('libs/javarosa-libraries-2014-04-29.jar') } diff --git a/third_party/odkcollect/src/main/res/drawable-xxhdpi/ic_launcher.png b/third_party/odkcollect/src/main/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index 4df18946442ed763bd52cf3adca31617848656fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19388 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 From 3e89deb5ae3d0ca1872d649267ce78baa007c60c Mon Sep 17 00:00:00 2001 From: Leonardo Lima de Vasconcellos Date: Tue, 19 Jan 2016 19:58:10 -0200 Subject: [PATCH 04/51] This fixes #225 --- app/src/main/assets/chart.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/chart.html b/app/src/main/assets/chart.html index 24379136..cdd57984 100644 --- a/app/src/main/assets/chart.html +++ b/app/src/main/assets/chart.html @@ -118,7 +118,7 @@
{{order.medication}}
-
{{order.dosage}} 
+
{{order.dosage}} {{order.frequency != null ? order.frequency + 'x daily' : ''}}
{% set previousActive = false %} {% set future = false %} From e9367621be97064b7299739af2b93980d4daf08a Mon Sep 17 00:00:00 2001 From: Dan Cunningham Date: Wed, 20 Jan 2016 13:22:20 +0000 Subject: [PATCH 05/51] Make start and stop (end) times for cell accessible via javascript --- app/src/main/assets/chart.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/assets/chart.html b/app/src/main/assets/chart.html index 24379136..df825fa4 100644 --- a/app/src/main/assets/chart.html +++ b/app/src/main/assets/chart.html @@ -77,6 +77,8 @@ {% set class = summaryValue | format_values(row.item.cssClass) %} {% set style = summaryValue | format_values(row.item.cssStyle) %} Values to a List of JsonObservations. This means that we can process them consistently to the way the Observation syncing code does. - Add a notes panel, which slides up from the bottom of the patient chart activity and allows notes to be entered by the user. --- app/build.gradle | 2 + .../events/data/EncounterAddFailedEvent.java | 5 +- .../client/json/JsonEncounter.java | 6 +- .../client/models/AppModel.java | 8 +- .../client/models/Encounter.java | 67 +++--- .../client/models/PatientDelta.java | 37 ++- .../client/models/tasks/AddEncounterTask.java | 32 ++- .../org/projectbuendia/client/net/Server.java | 4 +- .../client/ui/chart/PatientChartActivity.java | 110 ++++++++- .../ui/chart/PatientChartController.java | 64 ++++- .../chart/PatientObservationsListAdapter.java | 142 +++++++++++ .../client/ui/lists/LocationListFragment.java | 1 + .../res/layout/fragment_patient_chart.xml | 220 +++++++++++++----- .../notes_list_adapter_note_template.xml | 23 ++ app/src/main/res/values/colors.xml | 4 + app/src/main/res/values/dimens.xml | 14 ++ app/src/main/res/values/strings.xml | 12 + 17 files changed, 601 insertions(+), 150 deletions(-) create mode 100644 app/src/main/java/org/projectbuendia/client/ui/chart/PatientObservationsListAdapter.java create mode 100644 app/src/main/res/layout/notes_list_adapter_note_template.xml create mode 100644 app/src/main/res/values/dimens.xml diff --git a/app/build.gradle b/app/build.gradle index 99163db3..5fc6c6de 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,6 +63,8 @@ dependencies { compile 'com.mitchellbosecke:pebble:1.5.1' // HTML templating compile 'org.slf4j:slf4j-simple:1.7.12' // HTML templating dependency compile 'org.apache.commons:commons-lang3:3.4' + // Magic sliding panel that we use for the notes view. + compile 'com.sothree.slidinguppanel:library:3.2.1' // Testing androidTestCompile 'com.android.support.test:runner:0.3' diff --git a/app/src/main/java/org/projectbuendia/client/events/data/EncounterAddFailedEvent.java b/app/src/main/java/org/projectbuendia/client/events/data/EncounterAddFailedEvent.java index 546d4583..6da7bcd7 100644 --- a/app/src/main/java/org/projectbuendia/client/events/data/EncounterAddFailedEvent.java +++ b/app/src/main/java/org/projectbuendia/client/events/data/EncounterAddFailedEvent.java @@ -12,6 +12,7 @@ package org.projectbuendia.client.events.data; import org.projectbuendia.client.events.DefaultCrudEventBus; +import org.projectbuendia.client.models.Encounter; /** * An event bus event indicating that adding an encounter failed. @@ -21,6 +22,7 @@ public class EncounterAddFailedEvent { public final Reason reason; public final Exception exception; + public final Encounter encounter; public enum Reason { UNKNOWN, @@ -33,7 +35,8 @@ public enum Reason { FAILED_TO_FETCH_SAVED_OBSERVATION } - public EncounterAddFailedEvent(Reason reason, Exception exception) { + public EncounterAddFailedEvent(Encounter encounter, Reason reason, Exception exception) { + this.encounter = encounter; this.reason = reason; this.exception = exception; } diff --git a/app/src/main/java/org/projectbuendia/client/json/JsonEncounter.java b/app/src/main/java/org/projectbuendia/client/json/JsonEncounter.java index 8e75de1a..4e043b4e 100644 --- a/app/src/main/java/org/projectbuendia/client/json/JsonEncounter.java +++ b/app/src/main/java/org/projectbuendia/client/json/JsonEncounter.java @@ -13,15 +13,13 @@ import org.joda.time.DateTime; -import java.util.Map; +import java.util.List; /** JSON representation of an OpenMRS Encounter; call Serializers.registerTo before use. */ public class JsonEncounter { public String patient_uuid; public String uuid; public DateTime timestamp; - public String enterer_id; - /** A {conceptUuid: value} map, where value can be a number, string, or answer UUID. */ - public Map observations; + public List observations; public String[] order_uuids; // orders executed during this encounter } diff --git a/app/src/main/java/org/projectbuendia/client/models/AppModel.java b/app/src/main/java/org/projectbuendia/client/models/AppModel.java index 5eb7a738..c607f275 100644 --- a/app/src/main/java/org/projectbuendia/client/models/AppModel.java +++ b/app/src/main/java/org/projectbuendia/client/models/AppModel.java @@ -36,6 +36,8 @@ import org.projectbuendia.client.utils.Logger; import org.projectbuendia.client.utils.Utils; +import javax.annotation.Nullable; + import de.greenrobot.event.NoSubscriberEvent; /** @@ -194,10 +196,10 @@ public void deleteOrder(CrudEventBus bus, String orderUuid) { * Asynchronously adds an encounter that records an order as executed, posting a * {@link ItemCreatedEvent} when complete. */ - public void addOrderExecutedEncounter(CrudEventBus bus, Patient patient, String orderUuid) { + public void addOrderExecutedEncounter( + CrudEventBus bus, Patient patient, String orderUuid, @Nullable String userUuid) { addEncounter(bus, patient, new Encounter( - patient.uuid, null, DateTime.now(), null, new String[]{orderUuid} - )); + patient.uuid, null, DateTime.now(), null, new String[]{orderUuid}, userUuid)); } /** diff --git a/app/src/main/java/org/projectbuendia/client/models/Encounter.java b/app/src/main/java/org/projectbuendia/client/models/Encounter.java index 2a05ab19..5d8da8ec 100644 --- a/app/src/main/java/org/projectbuendia/client/models/Encounter.java +++ b/app/src/main/java/org/projectbuendia/client/models/Encounter.java @@ -18,14 +18,13 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.projectbuendia.client.net.Server; import org.projectbuendia.client.json.JsonEncounter; +import org.projectbuendia.client.json.JsonObservation; +import org.projectbuendia.client.net.Server; import org.projectbuendia.client.providers.Contracts.Observations; -import org.projectbuendia.client.utils.Logger; import java.util.ArrayList; import java.util.List; -import java.util.Map; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -37,10 +36,6 @@ *
* https://wiki.openmrs.org/display/docs/Encounters+and+observations" * - *

- *

NOTE: Because of lack of typing info from the server, {@link Encounter} attempts to - * determine the most appropriate type, but this typing is not guaranteed to succeed; also, - * currently only DATE and UUID (coded) types are supported. */ @Immutable public class Encounter extends Base { @@ -50,7 +45,7 @@ public class Encounter extends Base { public final DateTime timestamp; public final Observation[] observations; public final String[] orderUuids; - private static final Logger LOG = Logger.create(); + public final @Nullable String userUuid; /** * Creates a new Encounter for the given patient. @@ -65,13 +60,15 @@ public Encounter( @Nullable String encounterUuid, DateTime timestamp, Observation[] observations, - String[] orderUuids) { + String[] orderUuids, + @Nullable String userUuid) { id = encounterUuid; this.patientUuid = patientUuid; this.encounterUuid = id; this.timestamp = timestamp; this.observations = observations == null ? new Observation[] {} : observations; this.orderUuids = orderUuids == null ? new String[] {} : orderUuids; + this.userUuid = userUuid; } /** @@ -79,18 +76,17 @@ public Encounter( * {@link JsonEncounter} object and corresponding patient UUID. */ public static Encounter fromJson(String patientUuid, JsonEncounter encounter) { - List observations = new ArrayList(); + List observations = new ArrayList<>(); if (encounter.observations != null) { - for (Map.Entry observation : encounter.observations.entrySet()) { + for (JsonObservation observation : encounter.observations) { observations.add(new Observation( - (String) observation.getKey(), - (String) observation.getValue(), - Observation.estimatedTypeFor((String) observation.getValue()) + observation.concept_uuid, + observation.value )); } } return new Encounter(patientUuid, encounter.uuid, encounter.timestamp, - observations.toArray(new Observation[observations.size()]), encounter.order_uuids); + observations.toArray(new Observation[observations.size()]), encounter.order_uuids, null); } /** Serializes this into a {@link JSONObject}. */ @@ -101,12 +97,8 @@ public JSONObject toJson() throws JSONException { if (observations.length > 0) { JSONArray observationsJson = new JSONArray(); for (Observation obs : observations) { - JSONObject observationJson = new JSONObject(); - observationJson.put(Server.OBSERVATION_QUESTION_UUID, obs.conceptUuid); - String valueKey = obs.type == Observation.Type.DATE ? - Server.OBSERVATION_ANSWER_DATE : Server.OBSERVATION_ANSWER_UUID; - observationJson.put(valueKey, obs.value); - observationsJson.put(observationJson); + + observationsJson.put(obs.toJson()); } json.put(Server.ENCOUNTER_OBSERVATIONS_KEY, observationsJson); } @@ -117,6 +109,7 @@ public JSONObject toJson() throws JSONException { } json.put(Server.ENCOUNTER_ORDER_UUIDS, orderUuidsJson); } + json.put(Server.ENCOUNTER_USER_UUID, userUuid); return json; } @@ -153,31 +146,23 @@ public ContentValues[] toContentValuesArray() { public static final class Observation { public final String conceptUuid; public final String value; - public final Type type; - - /** Data type of the observation. */ - public enum Type { - DATE, - NON_DATE - } - public Observation(String conceptUuid, String value, Type type) { + public Observation(String conceptUuid, String value) { this.conceptUuid = conceptUuid; this.value = value; - this.type = type; } - /** - * Produces a best guess for the type of a given value, since the server doesn't give us - * typing information. - */ - public static Type estimatedTypeFor(String value) { + public JSONObject toJson() { + JSONObject observationJson = new JSONObject(); try { - new DateTime(Long.parseLong(value)); - return Type.DATE; - } catch (Exception e) { - return Type.NON_DATE; + observationJson.put(Server.OBSERVATION_QUESTION_UUID, conceptUuid); + observationJson.put(Server.OBSERVATION_ANSWER, value); + } catch (JSONException jsonException) { + // Should never occur, JSONException is only thrown for a null key or an invalid + // numeric value, neither of which will occur in this API. + throw new RuntimeException(jsonException); } + return observationJson; } } @@ -208,11 +193,11 @@ public Loader(String patientUuid) { String value = cursor.getString(cursor.getColumnIndex(Observations.VALUE)); observations.add(new Observation( cursor.getString(cursor.getColumnIndex(Observations.CONCEPT_UUID)), - value, Observation.estimatedTypeFor(value) + value )); } return new Encounter(mPatientUuid, encounterUuid, new DateTime(millis), - observations.toArray(new Observation[observations.size()]), null); + observations.toArray(new Observation[observations.size()]), null, null); } } } diff --git a/app/src/main/java/org/projectbuendia/client/models/PatientDelta.java b/app/src/main/java/org/projectbuendia/client/models/PatientDelta.java index 0cf8b7a7..50b839c9 100644 --- a/app/src/main/java/org/projectbuendia/client/models/PatientDelta.java +++ b/app/src/main/java/org/projectbuendia/client/models/PatientDelta.java @@ -15,13 +15,12 @@ import com.google.common.base.Optional; -import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.projectbuendia.client.net.Server; import org.projectbuendia.client.json.JsonPatient; +import org.projectbuendia.client.net.Server; import org.projectbuendia.client.providers.Contracts; import org.projectbuendia.client.utils.Logger; import org.projectbuendia.client.utils.Utils; @@ -103,24 +102,24 @@ public boolean toJson(JSONObject json) { JSONArray observations = new JSONArray(); if (admissionDate.isPresent()) { - JSONObject observation = new JSONObject(); - observation.put(Server.OBSERVATION_QUESTION_UUID, ConceptUuids.ADMISSION_DATE_UUID); - observation.put( - Server.OBSERVATION_ANSWER_DATE, - Utils.toString(admissionDate.get())); - observations.put(observation); + JSONObject jsonObs = + new Encounter.Observation( + ConceptUuids.ADMISSION_DATE_UUID, + Utils.toString(admissionDate.get())) + .toJson(); + + observations.put(jsonObs); } if (firstSymptomDate.isPresent()) { - JSONObject observation = new JSONObject(); - observation.put(Server.OBSERVATION_QUESTION_UUID, ConceptUuids.FIRST_SYMPTOM_DATE_UUID); - observation.put( - Server.OBSERVATION_ANSWER_DATE, - Utils.toString(firstSymptomDate.get())); - observations.put(observation); - } - if (observations != null) { - json.put(Server.ENCOUNTER_OBSERVATIONS_KEY, observations); + JSONObject jsonObs = + new Encounter.Observation( + ConceptUuids.FIRST_SYMPTOM_DATE_UUID, + Utils.toString(firstSymptomDate.get())) + .toJson(); + + observations.put(jsonObs); } + json.put(Server.ENCOUNTER_OBSERVATIONS_KEY, observations); if (assignedLocationUuid.isPresent()) { json.put( @@ -141,8 +140,4 @@ private static JSONObject getLocationObject(String assignedLocationUuid) throws location.put("uuid", assignedLocationUuid); return location; } - - private static long getTimestamp(DateTime dateTime) { - return dateTime.toInstant().getMillis()/1000; - } } diff --git a/app/src/main/java/org/projectbuendia/client/models/tasks/AddEncounterTask.java b/app/src/main/java/org/projectbuendia/client/models/tasks/AddEncounterTask.java index 66a48658..0503d076 100644 --- a/app/src/main/java/org/projectbuendia/client/models/tasks/AddEncounterTask.java +++ b/app/src/main/java/org/projectbuendia/client/models/tasks/AddEncounterTask.java @@ -90,7 +90,8 @@ public AddEncounterTask( try { jsonEncounter = future.get(); } catch (InterruptedException e) { - return new EncounterAddFailedEvent(EncounterAddFailedEvent.Reason.INTERRUPTED, e); + return new EncounterAddFailedEvent( + mEncounter, EncounterAddFailedEvent.Reason.INTERRUPTED, e); } catch (ExecutionException e) { LOG.e(e, "Server error while adding encounter"); @@ -106,7 +107,8 @@ public AddEncounterTask( } LOG.e("Error response: %s", ((VolleyError) e.getCause()).networkResponse); - return new EncounterAddFailedEvent(reason, (VolleyError) e.getCause()); + return new EncounterAddFailedEvent( + mEncounter, reason, (VolleyError) e.getCause()); } if (jsonEncounter.uuid == null) { @@ -114,10 +116,16 @@ public AddEncounterTask( "Although the server reported an encounter successfully added, it did not " + "return a UUID for that encounter. This indicates a server error."); - return new EncounterAddFailedEvent( - EncounterAddFailedEvent.Reason.FAILED_TO_SAVE_ON_SERVER, null /*exception*/); + return new EncounterAddFailedEvent(mEncounter, + EncounterAddFailedEvent.Reason.FAILED_TO_SAVE_ON_SERVER, null /*exception*/); } + // TODO: the encounter database saving code here doesn't correctly attribute observations to + // the user that created them, despite the fact that this data is sent from the server. + // This will be remedied on the next sync. + // Instead of adding a workaround here, we should unify the code that deals with + // observations as part of encounters and the code that deals with observations as entities + // that get synced. Encounter encounter = Encounter.fromJson(mPatient.uuid, jsonEncounter); ContentValues[] values = encounter.toContentValuesArray(); if (values.length > 0) { @@ -126,9 +134,9 @@ public AddEncounterTask( if (inserted != values.length) { LOG.w("Inserted %d observations for encounter. Expected: %d", inserted, encounter.observations.length); - return new EncounterAddFailedEvent( - EncounterAddFailedEvent.Reason.INVALID_NUMBER_OF_OBSERVATIONS_SAVED, - null /*exception*/); + return new EncounterAddFailedEvent(mEncounter, + EncounterAddFailedEvent.Reason.INVALID_NUMBER_OF_OBSERVATIONS_SAVED, + null /*exception*/); } } else { LOG.w("Encounter was sent to the server but contained no observations."); @@ -151,8 +159,8 @@ public AddEncounterTask( "Although an encounter add ostensibly succeeded, no UUID was set for the newly-" + "added encounter. This indicates a programming error."); - mBus.post(new EncounterAddFailedEvent( - EncounterAddFailedEvent.Reason.UNKNOWN, null /*exception*/)); + mBus.post(new EncounterAddFailedEvent(mEncounter, + EncounterAddFailedEvent.Reason.UNKNOWN, null /*exception*/)); return; } @@ -179,9 +187,9 @@ public void onEventMainThread(ItemFetchedEvent event) { } public void onEventMainThread(ItemFetchFailedEvent event) { - mBus.post(new EncounterAddFailedEvent( - EncounterAddFailedEvent.Reason.FAILED_TO_FETCH_SAVED_OBSERVATION, - new Exception(event.error))); + mBus.post(new EncounterAddFailedEvent(mEncounter, + EncounterAddFailedEvent.Reason.FAILED_TO_FETCH_SAVED_OBSERVATION, + new Exception(event.error))); mBus.unregister(this); } } diff --git a/app/src/main/java/org/projectbuendia/client/net/Server.java b/app/src/main/java/org/projectbuendia/client/net/Server.java index 4c1e6353..c83e4bb0 100644 --- a/app/src/main/java/org/projectbuendia/client/net/Server.java +++ b/app/src/main/java/org/projectbuendia/client/net/Server.java @@ -42,9 +42,9 @@ public interface Server { public static final String ENCOUNTER_OBSERVATIONS_KEY = "observations"; public static final String ENCOUNTER_TIMESTAMP = "timestamp"; public static final String ENCOUNTER_ORDER_UUIDS = "order_uuids"; + public static final String ENCOUNTER_USER_UUID = "enterer_uuid"; public static final String OBSERVATION_QUESTION_UUID = "question_uuid"; - public static final String OBSERVATION_ANSWER_DATE = "answer_date"; - public static final String OBSERVATION_ANSWER_UUID = "answer_uuid"; + public static final String OBSERVATION_ANSWER = "answer_value"; /** * Logs an event by sending a dummy request to the server. (The server logs diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java index 3888f8ab..f3fed7bd 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java @@ -12,24 +12,34 @@ package org.projectbuendia.client.ui.chart; import android.app.ActionBar; +import android.app.LoaderManager; import android.app.ProgressDialog; import android.content.Context; +import android.content.CursorLoader; import android.content.Intent; +import android.content.Loader; +import android.database.Cursor; import android.graphics.Point; import android.os.Bundle; import android.os.Handler; +import android.text.Editable; +import android.text.TextWatcher; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; import android.webkit.WebView; import android.webkit.WebViewClient; +import android.widget.EditText; +import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; import com.google.common.base.Joiner; import com.joanzapata.android.iconify.IconDrawable; import com.joanzapata.android.iconify.Iconify; +import com.sothree.slidinguppanel.SlidingUpPanelLayout; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -104,13 +114,17 @@ public final class PatientChartActivity extends BaseLoggedInActivity { @Inject SyncManager mSyncManager; @Inject ChartDataHelper mChartDataHelper; @Inject AppSettings mSettings; - @InjectView(R.id.patient_chart_root) ViewGroup mRootView; + @InjectView(R.id.patient_chart_root) SlidingUpPanelLayout mRootView; @InjectView(R.id.attribute_location) PatientAttributeView mPatientLocationView; @InjectView(R.id.attribute_admission_days) PatientAttributeView mAdmissionDaysView; @InjectView(R.id.attribute_symptoms_onset_days) PatientAttributeView mSymptomOnsetDaysView; @InjectView(R.id.attribute_pcr) PatientAttributeView mPcr; @InjectView(R.id.patient_chart_pregnant) TextView mPatientPregnantOrIvView; @InjectView(R.id.chart_webview) WebView mGridWebView; + @InjectView(R.id.notes_panel_list) ListView mNotesList; + @InjectView(R.id.notes_panel_text_entry) EditText mAddNoteEntryText; + @InjectView(R.id.notes_panel_btn_save) View mAddNoteButton; + @InjectView(R.id.notes_panel_submit_spinner) View mAddNoteWaitingSpinner; private static final String EN_DASH = "\u2013"; @@ -194,6 +208,16 @@ public static void start(Context caller, String uuid) { return super.onOptionsItemSelected(item); } + @Override + public void onBackPressed() { + // If the notes view is open, collapse it before navigating back up to the parent activity. + if (mRootView.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + mRootView.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + } else { + super.onBackPressed(); + } + } + @Override protected void onCreateImpl(Bundle savedInstanceState) { super.onCreateImpl(savedInstanceState); setContentView(R.layout.fragment_patient_chart); @@ -274,6 +298,72 @@ public void onPageFinished(WebView view, String url) { }); initChartMenu(); + + // Hide IME if the notes panel closes. + mRootView.setPanelSlideListener(new SlidingUpPanelLayout.SimplePanelSlideListener() { + @Override + public void onPanelCollapsed(View panel) { + View view = getCurrentFocus(); + if (view != null) { + InputMethodManager imm = + (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + }); + // Set up an adapter for the notes list, and register callbacks with the LoaderManager + // so that the list updates automatically. + PatientObservationsListAdapter adapter = new PatientObservationsListAdapter(this); + mNotesList.setAdapter(adapter); + getLoaderManager().initLoader(0, null, + new PatientObservationsListAdapter.ObservationsListLoaderCallbacks( + this, + getIntent().getStringExtra("uuid"), + ConceptUuids.NOTES_UUID, + adapter)); + + mNotesList.setEmptyView(findViewById(R.id.notes_panel_list_empty)); + mAddNoteEntryText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + mAddNoteButton.setEnabled(s.length() > 0); + } + }); + // Trigger the text changed listener. + mAddNoteEntryText.setText(""); + setNoteSubmissionState(false); + + mAddNoteButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mController.addNote(mAddNoteEntryText.getText().toString()); + // Lock out the text box and the button. + setNoteSubmissionState(true); + } + }); + } + + private void setNoteSubmissionState(boolean isSubmitting) { + if (isSubmitting) { + // Replace the "Submit" button with a spinner + mAddNoteButton.setVisibility(View.INVISIBLE); + mAddNoteWaitingSpinner.setVisibility(View.VISIBLE); + // Disable text entry. + mAddNoteEntryText.setEnabled(false); + } else { + mAddNoteButton.setVisibility(View.VISIBLE); + mAddNoteWaitingSpinner.setVisibility(View.INVISIBLE); + // Enable text entry. + mAddNoteEntryText.setEnabled(true); + } } private void initChartMenu() { @@ -517,6 +607,22 @@ public void updatePatientLocationUi(LocationTree locationTree, Patient patient) .show(getSupportFragmentManager(), null); } + @Override + public void indicateNoteSubmitted() { + setNoteSubmissionState(false); + mAddNoteEntryText.setText(""); + //TODO: scroll to bottom to show the newly added note. + } + + @Override + public void indicateNoteSubmissionFailed() { + setNoteSubmissionState(false); + Toast.makeText( + PatientChartActivity.this, + "Failed to submit note.", + Toast.LENGTH_SHORT).show(); + } + @Override public void showOrderExecutionDialog( Order order, Interval interval, List executionTimes) { OrderExecutionDialogFragment.newInstance(order, interval, executionTimes) diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java index 3da03e12..12b58605 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java @@ -38,6 +38,7 @@ import org.projectbuendia.client.events.actions.VoidObservationsRequestEvent; import org.projectbuendia.client.events.data.AppLocationTreeFetchedEvent; import org.projectbuendia.client.events.data.EncounterAddFailedEvent; +import org.projectbuendia.client.events.data.ItemCreatedEvent; import org.projectbuendia.client.events.data.ItemDeletedEvent; import org.projectbuendia.client.events.data.ItemFetchedEvent; import org.projectbuendia.client.events.data.PatientUpdateFailedEvent; @@ -74,7 +75,6 @@ final class PatientChartController implements ChartRenderer.GridJsInterface { private static final Logger LOG = Logger.create(); - private static final boolean DEBUG = true; private static final String KEY_PENDING_UUIDS = "pendingUuids"; // Form UUIDs specific to Ebola deployments. @@ -99,7 +99,6 @@ final class PatientChartController implements ChartRenderer.GridJsInterface { // the savedInstanceState. // TODO: Use a map for this instead of an array. private final String[] mPatientUuids; - private int mNextIndex = 0; private Patient mPatient = Patient.builder().build(); private LocationTree mLocationTree; @@ -129,6 +128,8 @@ final class PatientChartController implements ChartRenderer.GridJsInterface { // Store chart's last scroll position private Point mLastScrollPosition; + private Encounter mPendingNotesEncounter; + public Point getLastScrollPosition() { return mLastScrollPosition; } @@ -185,6 +186,8 @@ void showOrderExecutionDialog(Order order, Interval interval, List executionTimes); void showEditPatientDialog(Patient patient); void showObservationsDialog(ArrayList obs); + void indicateNoteSubmitted(); + void indicateNoteSubmissionFailed(); } /** Sends ODK form data. */ @@ -361,6 +364,25 @@ public void onEditPatientPressed() { mUi.showEditPatientDialog(mPatient); } + public void addNote(String note) { + Observation observation = new Observation( + ConceptUuids.NOTES_UUID, + note); + JsonUser user = App.getUserManager().getActiveUser(); + String userId = user == null ? null : user.id; + mPendingNotesEncounter = new Encounter( + mPatientUuid, + null, // Encounter UUID + DateTime.now(), + new Observation[]{observation}, + null, // Order UUIDs + userId); + mAppModel.addEncounter( + mCrudEventBus, + mPatient, + mPendingNotesEncounter); + } + private boolean dialogShowing() { return (mAssignGeneralConditionDialog != null && mAssignGeneralConditionDialog.isShowing()) || (mAssignLocationDialog != null && mAssignLocationDialog.isShowing()); @@ -482,6 +504,8 @@ public void showAssignGeneralConditionDialog( public void setCondition(String newConditionUuid) { LOG.v("Assigning general condition: %s", newConditionUuid); + JsonUser user = App.getUserManager().getActiveUser(); + String userId = user == null ? null : user.id; Encounter encounter = new Encounter( mPatientUuid, null, // encounter UUID, which the server will generate @@ -489,9 +513,8 @@ public void setCondition(String newConditionUuid) { new Observation[] { new Observation( ConceptUuids.GENERAL_CONDITION_UUID, - newConditionUuid, - Observation.Type.NON_DATE) - }, null); + newConditionUuid) + }, null, userId); mAppModel.addEncounter(mCrudEventBus, mPatient, encounter); } @@ -602,6 +625,12 @@ public void onEventMainThread(SyncSucceededEvent event) { } public void onEventMainThread(EncounterAddFailedEvent event) { + if (event.encounter == mPendingNotesEncounter) { + mUi.indicateNoteSubmissionFailed(); + mPendingNotesEncounter = null; + return; + } + if (mAssignGeneralConditionDialog != null) { mAssignGeneralConditionDialog.dismiss(); mAssignGeneralConditionDialog = null; @@ -639,6 +668,27 @@ public void onEventMainThread(EncounterAddFailedEvent event) { mUi.showError(messageResource, exceptionMessage); } + public void onEventMainThread(ItemCreatedEvent event) { + if (objectIsNoteCreationEncounter(event.item)) { + mUi.indicateNoteSubmitted(); + mPendingNotesEncounter = null; + } + } + + /** + * There's no reference equality after an item has been created, and our data model + * is a mess so we can't use .equals(), so we do a "close enough" comparison to work out + * if a note was submitted. + */ + private boolean objectIsNoteCreationEncounter(Object object) { + if (!(object instanceof Encounter)) { + return false; + } + Encounter encounter = (Encounter) object; + return encounter.observations.length != 0 + && ConceptUuids.NOTES_UUID.equals(encounter.observations[0].conceptUuid); + } + // We get a ItemFetchedEvent when the initial patient data is loaded // from SQLite or after an edit has been successfully posted to the server. public void onEventMainThread(ItemFetchedEvent event) { @@ -787,7 +837,9 @@ public void onEventMainThread(VoidObservationsRequestEvent event) { public void onEventMainThread(OrderExecutionSaveRequestedEvent event) { Order order = mOrdersByUuid.get(event.orderUuid); if (order != null) { - mAppModel.addOrderExecutedEncounter(mCrudEventBus, mPatient, order.uuid); + JsonUser user = App.getUserManager().getActiveUser(); + String userId = user == null ? null : user.id; + mAppModel.addOrderExecutedEncounter(mCrudEventBus, mPatient, order.uuid, userId); } } } diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientObservationsListAdapter.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientObservationsListAdapter.java new file mode 100644 index 00000000..e4eecf37 --- /dev/null +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientObservationsListAdapter.java @@ -0,0 +1,142 @@ +package org.projectbuendia.client.ui.chart; + +import android.app.LoaderManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.database.Cursor; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.TextView; + +import org.projectbuendia.client.R; +import org.projectbuendia.client.providers.Contracts; +import org.projectbuendia.client.providers.Contracts.Observations; + +import java.util.Date; + +/** + * A {@link android.widget.ListAdapter} that displays observations for a given patient, matching a + * given concept UUID. + *

+ * TODO: This adapter currently does some database queries on the main thread - we should + * offload these to a background thread for performance reasons. + */ +public class PatientObservationsListAdapter extends CursorAdapter { + + private static final String[] PROJECTION = new String[] { + "rowid AS _id", + Observations.ENTERER_UUID, + Observations.ENCOUNTER_MILLIS, + Observations.VALUE, + }; + + private final ContentResolver mContentResolver; + + public PatientObservationsListAdapter(Context context) { + super(context, null, 0); + mContentResolver = context.getContentResolver(); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(context); + return inflater.inflate(R.layout.notes_list_adapter_note_template, parent, false); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + // Obtain data from cursor + Date encounterTimestamp = new Date(cursor.getLong( + cursor.getColumnIndexOrThrow(Observations.ENCOUNTER_MILLIS))); + String value = cursor.getString( + cursor.getColumnIndexOrThrow(Observations.VALUE)); + String entererUuid = cursor.getString( + cursor.getColumnIndexOrThrow(Observations.ENTERER_UUID)); + String enterer = getUsersNameFromUuid(entererUuid); + + // Obtain view references + ViewGroup viewGroup = (ViewGroup) view; + TextView metaLine = (TextView) viewGroup.findViewById(R.id.meta); + TextView content = (TextView) viewGroup.findViewById(R.id.observation_content); + + // Set content + metaLine.setText(context.getResources().getString( + enterer == null + ? R.string.notes_list_metadata_format_no_user_info + : R.string.notes_list_metadata_format, + encounterTimestamp, enterer)); + content.setText(value); + } + + /** + * Returns the users' full name from a UUID. Note that this performs a database query, and so + * ideally calls should be kept off the main thread. It does not perform a network request to + * check for new users on the server. + * + * @param uuid The uuid of the user whose name to return. Note that if {@code null} is passed, + * {@code null} will be returned. + * @return the users' name, if a user was found matching this UUID. {@code null} otherwise. + */ + public @Nullable String getUsersNameFromUuid(@Nullable String uuid) { + if (uuid == null) { + return null; + } + try (Cursor cursor = mContentResolver.query( + Contracts.Users.CONTENT_URI.buildUpon().appendPath(uuid).build(), + new String[]{Contracts.Users.FULL_NAME}, + null, + null, + null)) { + if (cursor == null || !cursor.moveToFirst()) { + // Either there wasn't a cursor, or the result set was empty. + // This is a user we don't know about. + return null; + } + return cursor.getString(0); + } + } + + public static class ObservationsListLoaderCallbacks + implements LoaderManager.LoaderCallbacks { + + private final Context mContext; + private final String mPatientUuid; + private final String mConceptUuid; + private final CursorAdapter mAdapter; + + public ObservationsListLoaderCallbacks( + Context context, String patientUuid, String conceptUuid, CursorAdapter adapter) { + mContext = context; + mPatientUuid = patientUuid; + mConceptUuid = conceptUuid; + mAdapter = adapter; + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new CursorLoader(mContext, + Observations.CONTENT_URI, + PROJECTION, + Observations.PATIENT_UUID + " = ? AND " + + Observations.CONCEPT_UUID + " = ? ", + new String[]{mPatientUuid, mConceptUuid}, + Observations.ENCOUNTER_MILLIS); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + mAdapter.swapCursor(data); + } + + @Override + public void onLoaderReset(Loader loader) { + mAdapter.swapCursor(null); + } + } +} diff --git a/app/src/main/java/org/projectbuendia/client/ui/lists/LocationListFragment.java b/app/src/main/java/org/projectbuendia/client/ui/lists/LocationListFragment.java index b150ef9e..ccf6c5dd 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/lists/LocationListFragment.java +++ b/app/src/main/java/org/projectbuendia/client/ui/lists/LocationListFragment.java @@ -12,6 +12,7 @@ package org.projectbuendia.client.ui.lists; import android.os.Bundle; +import android.os.Debug; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; diff --git a/app/src/main/res/layout/fragment_patient_chart.xml b/app/src/main/res/layout/fragment_patient_chart.xml index ef05ba39..4abfb802 100644 --- a/app/src/main/res/layout/fragment_patient_chart.xml +++ b/app/src/main/res/layout/fragment_patient_chart.xml @@ -9,69 +9,173 @@ OR CONDITIONS OF ANY KIND, either express or implied. See the License for specific language governing permissions and limitations under the License. --> - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +