├── .gitignore ├── LICENSE ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── app │ │ └── bandemic │ │ └── InfectedUUIDDatabaseMatchTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── app │ │ │ └── bandemic │ │ │ ├── fragments │ │ │ ├── EnvironmentLoggerFragment.java │ │ │ ├── ErrorMessageFragment.java │ │ │ ├── InfectionCheckFragment.java │ │ │ └── NearbyDevicesFragment.java │ │ │ ├── strict │ │ │ ├── database │ │ │ │ ├── AppDatabase.java │ │ │ │ ├── Beacon.java │ │ │ │ ├── BeaconDao.java │ │ │ │ ├── Converters.java │ │ │ │ ├── InfectedUUID.java │ │ │ │ ├── InfectedUUIDDao.java │ │ │ │ ├── Infection.java │ │ │ │ ├── OwnUUID.java │ │ │ │ └── OwnUUIDDao.java │ │ │ ├── network │ │ │ │ ├── HexStringToByteArrayTypeAdapter.java │ │ │ │ ├── InfectedUUIDResponse.java │ │ │ │ ├── InfectionchainWebservice.java │ │ │ │ └── RetrofitClient.java │ │ │ ├── repository │ │ │ │ ├── BroadcastRepository.java │ │ │ │ └── InfectedUUIDRepository.java │ │ │ └── service │ │ │ │ ├── BandemicProfile.java │ │ │ │ ├── BeaconCache.java │ │ │ │ ├── BleAdvertiser.java │ │ │ │ ├── BleScanner.java │ │ │ │ ├── StartupListener.java │ │ │ │ └── TracingService.java │ │ │ ├── ui │ │ │ ├── DataProtectionInfo.java │ │ │ ├── EnvironmentDevicesAdapter.java │ │ │ ├── InfectedUUIDsAdapter.java │ │ │ ├── Instructions.java │ │ │ └── MainActivity.java │ │ │ └── viewmodel │ │ │ ├── EnvironmentLoggerViewModel.java │ │ │ ├── InfectionCheckViewModel.java │ │ │ ├── MainActivityViewModel.java │ │ │ └── NearbyDevicesViewModel.java │ └── res │ │ ├── drawable-hdpi │ │ ├── wvvszene2und3.jpg │ │ ├── wvvszene4.jpg │ │ └── wwwszene1.jpg │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_icon_bg.xml │ │ ├── ic_icon_fg.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_status_error.xml │ │ ├── ic_status_ok.xml │ │ ├── ic_status_time.xml │ │ └── nearby_device.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── data_protection_info.xml │ │ ├── environment_logger_fragment.xml │ │ ├── fragment_error_message.xml │ │ ├── infected_encounter_view.xml │ │ ├── infected_info.xml │ │ ├── infection_check_fragment.xml │ │ ├── instructions.xml │ │ ├── nearby_devices_fragment.xml │ │ └── nearby_devices_view.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── values-de │ │ └── strings.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── app │ └── bandemic │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | /app/release 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bandemic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | buildToolsVersion "29.0.2" 6 | 7 | defaultConfig { 8 | applicationId "app.bandemic" 9 | minSdkVersion 21 10 | targetSdkVersion 29 11 | versionCode 1 12 | versionName "0.1" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility = 1.8 25 | targetCompatibility = 1.8 26 | } 27 | 28 | } 29 | 30 | dependencies { 31 | implementation fileTree(dir: 'libs', include: ['*.jar']) 32 | 33 | // Room 34 | def room_version = "2.2.5" 35 | 36 | implementation "androidx.room:room-runtime:$room_version" 37 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 38 | implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' 39 | annotationProcessor "androidx.room:room-compiler:$room_version" 40 | 41 | implementation 'com.squareup.retrofit2:retrofit:2.7.2' 42 | implementation 'com.squareup.retrofit2:converter-gson:2.7.2' 43 | implementation 'commons-codec:commons-codec:1.14' 44 | 45 | implementation 'androidx.appcompat:appcompat:1.1.0' 46 | implementation "androidx.preference:preference:1.1.0" 47 | implementation 'com.google.android.material:material:1.1.0' 48 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 49 | implementation 'androidx.navigation:navigation-fragment:2.2.1' 50 | implementation 'androidx.navigation:navigation-ui:2.2.1' 51 | testImplementation 'junit:junit:4.13' 52 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 53 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 54 | androidTestImplementation "androidx.arch.core:core-testing:2.1.0" 55 | 56 | def dynamicanimation_version = "1.0.0" 57 | implementation "androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version" 58 | 59 | } 60 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/bandemic/InfectedUUIDDatabaseMatchTest.java: -------------------------------------------------------------------------------- 1 | package app.bandemic; 2 | 3 | import android.content.Context; 4 | 5 | import app.bandemic.strict.database.AppDatabase; 6 | import app.bandemic.strict.database.Beacon; 7 | import app.bandemic.strict.database.BeaconDao; 8 | import app.bandemic.strict.database.InfectedUUID; 9 | import app.bandemic.strict.database.InfectedUUIDDao; 10 | 11 | import org.apache.commons.codec.binary.Hex; 12 | import org.junit.After; 13 | import org.junit.Before; 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | 18 | import java.io.IOException; 19 | import java.security.MessageDigest; 20 | import java.security.NoSuchAlgorithmException; 21 | import java.util.Date; 22 | import java.util.UUID; 23 | 24 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule; 25 | import androidx.room.Room; 26 | import androidx.test.core.app.ApplicationProvider; 27 | import androidx.test.ext.junit.runners.AndroidJUnit4; 28 | 29 | import static org.junit.Assert.assertEquals; 30 | 31 | @RunWith(AndroidJUnit4.class) 32 | public class InfectedUUIDDatabaseMatchTest { 33 | private BeaconDao beaconDao; 34 | private InfectedUUIDDao infectedUUIDDao; 35 | private AppDatabase db; 36 | 37 | @Rule 38 | public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule(); 39 | 40 | @Before 41 | public void createDb() { 42 | Context context = ApplicationProvider.getApplicationContext(); 43 | db = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build(); 44 | beaconDao = db.beaconDao(); 45 | infectedUUIDDao = db.infectedUUIDDao(); 46 | } 47 | 48 | @After 49 | public void closeDb() throws IOException { 50 | db.close(); 51 | } 52 | 53 | @Test 54 | public void writeUserAndReadInList() throws Exception { 55 | byte[] hash = Hex.decodeHex("2863284b83f4ec64223a47b62d5846533075b192ef53569525b1501371d23da7".toCharArray()); 56 | 57 | MessageDigest digest = null; 58 | try { 59 | digest = MessageDigest.getInstance("SHA-256"); 60 | } catch (NoSuchAlgorithmException e) { 61 | e.printStackTrace(); 62 | } 63 | 64 | 65 | InfectedUUID infected = new InfectedUUID( 66 | 0, 67 | new Date(), 68 | 0, 69 | digest.digest(hash), 70 | "Covid-19" 71 | ); 72 | 73 | Beacon beacon = new Beacon( 74 | hash, 75 | new Date(), 76 | 12 77 | ); 78 | 79 | infectedUUIDDao.insertAll(infected); 80 | beaconDao.insertAll(beacon); 81 | 82 | infectedUUIDDao.getPossiblyInfectedEncounters().observeForever(infectedUUIDS -> { 83 | // one infection should be found 84 | assertEquals(1, infectedUUIDS.size()); 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/fragments/EnvironmentLoggerFragment.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.fragments; 2 | 3 | 4 | import androidx.lifecycle.Observer; 5 | import androidx.lifecycle.ViewModelProviders; 6 | 7 | import android.graphics.Color; 8 | import android.os.Bundle; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.annotation.Nullable; 12 | import androidx.fragment.app.Fragment; 13 | import androidx.recyclerview.widget.LinearLayoutManager; 14 | import androidx.recyclerview.widget.RecyclerView; 15 | 16 | import android.view.LayoutInflater; 17 | import android.view.View; 18 | import android.view.ViewGroup; 19 | import androidx.cardview.widget.CardView; 20 | import android.widget.LinearLayout; 21 | 22 | import app.bandemic.R; 23 | import app.bandemic.strict.database.Beacon; 24 | import app.bandemic.ui.EnvironmentDevicesAdapter; 25 | import app.bandemic.viewmodel.EnvironmentLoggerViewModel; 26 | 27 | import java.util.List; 28 | 29 | public class EnvironmentLoggerFragment extends Fragment { 30 | 31 | private EnvironmentLoggerViewModel mViewModel; 32 | 33 | private RecyclerView recyclerView; 34 | private EnvironmentDevicesAdapter mAdapter; 35 | private RecyclerView.LayoutManager layoutManager; 36 | private LinearLayout noInfectionInformation; 37 | private CardView cardView; 38 | 39 | 40 | public static EnvironmentLoggerFragment newInstance() { 41 | return new EnvironmentLoggerFragment(); 42 | } 43 | 44 | @Override 45 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 46 | @Nullable Bundle savedInstanceState) { 47 | return inflater.inflate(R.layout.environment_logger_fragment, container, false); 48 | } 49 | 50 | @Override 51 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 52 | super.onViewCreated(view, savedInstanceState); 53 | recyclerView = view.findViewById(R.id.environment_logger_list_recycler_view); 54 | noInfectionInformation = view.findViewById(R.id.layout_no_detections); 55 | recyclerView.setHasFixedSize(true); 56 | layoutManager = new LinearLayoutManager(getActivity()); 57 | recyclerView.setLayoutManager(layoutManager); 58 | mAdapter = new EnvironmentDevicesAdapter(); 59 | recyclerView.setAdapter(mAdapter); 60 | cardView = view.findViewById(R.id.environmentCard); 61 | } 62 | 63 | @Override 64 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 65 | super.onActivityCreated(savedInstanceState); 66 | 67 | //View cardLayout = cardView.findViewById(R.id.environmentCard); 68 | mViewModel = ViewModelProviders.of(this).get(EnvironmentLoggerViewModel.class); 69 | cardView.setCardBackgroundColor(getResources().getColor(R.color.colorNoDanger)); 70 | 71 | mViewModel.getDistinctBeacons().observe(getViewLifecycleOwner(), new Observer>() { 72 | @Override 73 | public void onChanged(List beacons) { 74 | if(beacons.size() != 0) { 75 | mAdapter.setBeacons(beacons); 76 | noInfectionInformation.setVisibility(View.GONE); 77 | recyclerView.setVisibility(View.VISIBLE); 78 | if (beacons.size() >= 1) { //todo change to 5 (1 only for testing) 79 | cardView.setCardBackgroundColor(getResources().getColor(R.color.colorDanger)); 80 | } else if (beacons.size() > 10) { 81 | cardView.setCardBackgroundColor(getResources().getColor(R.color.colorRealDanger)); 82 | } 83 | } 84 | else { 85 | noInfectionInformation.setVisibility(View.VISIBLE); 86 | recyclerView.setVisibility(View.GONE); 87 | cardView.setCardBackgroundColor(getResources().getColor(R.color.colorNoDanger)); 88 | 89 | } 90 | } 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/fragments/ErrorMessageFragment.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.fragments; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.annotation.Nullable; 8 | import androidx.fragment.app.Fragment; 9 | 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.TextView; 14 | 15 | import app.bandemic.R; 16 | 17 | public class ErrorMessageFragment extends Fragment { 18 | private static final String ARG_ERROR_MESSAGE = "error_message"; 19 | 20 | private String errorMessage = ""; 21 | 22 | public ErrorMessageFragment() { 23 | // Required empty public constructor 24 | } 25 | 26 | public static ErrorMessageFragment newInstance(String errorMessage) { 27 | ErrorMessageFragment fragment = new ErrorMessageFragment(); 28 | Bundle args = new Bundle(); 29 | args.putString(ARG_ERROR_MESSAGE, errorMessage); 30 | fragment.setArguments(args); 31 | return fragment; 32 | } 33 | 34 | @Override 35 | public void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | if (getArguments() != null) { 38 | errorMessage = getArguments().getString(ARG_ERROR_MESSAGE); 39 | } 40 | } 41 | 42 | @Override 43 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 44 | Bundle savedInstanceState) { 45 | // Inflate the layout for this fragment 46 | return inflater.inflate(R.layout.fragment_error_message, container, false); 47 | } 48 | 49 | @Override 50 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 51 | super.onViewCreated(view, savedInstanceState); 52 | 53 | TextView textView = view.findViewById(R.id.error_message_txt); 54 | textView.setText(errorMessage); 55 | } 56 | 57 | @Override 58 | public void onAttach(Context context) { 59 | super.onAttach(context); 60 | } 61 | 62 | @Override 63 | public void onDetach() { 64 | super.onDetach(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/fragments/InfectionCheckFragment.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.fragments; 2 | 3 | import androidx.lifecycle.ViewModelProviders; 4 | 5 | import android.content.Intent; 6 | import android.graphics.Color; 7 | import android.os.Bundle; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | import androidx.fragment.app.Fragment; 12 | import androidx.recyclerview.widget.LinearLayoutManager; 13 | import androidx.recyclerview.widget.RecyclerView; 14 | 15 | import android.view.LayoutInflater; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.widget.LinearLayout; 19 | import androidx.cardview.widget.CardView; 20 | 21 | import app.bandemic.R; 22 | import app.bandemic.ui.InfectedUUIDsAdapter; 23 | import app.bandemic.viewmodel.InfectionCheckViewModel; 24 | import app.bandemic.viewmodel.MainActivityViewModel; 25 | 26 | public class InfectionCheckFragment extends Fragment { 27 | 28 | private InfectionCheckViewModel mViewModel; 29 | private MainActivityViewModel mainActivityViewModel; 30 | 31 | private RecyclerView recyclerView; 32 | private InfectedUUIDsAdapter mAdapter; 33 | private RecyclerView.LayoutManager layoutManager; 34 | private LinearLayout noInfectionInformation; 35 | private CardView cardView; 36 | 37 | public static InfectionCheckFragment newInstance() { 38 | return new InfectionCheckFragment(); 39 | } 40 | 41 | @Override 42 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 43 | @Nullable Bundle savedInstanceState) { 44 | return inflater.inflate(R.layout.infection_check_fragment, container, false); 45 | } 46 | 47 | @Override 48 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 49 | super.onViewCreated(view, savedInstanceState); 50 | recyclerView = view.findViewById(R.id.infection_check_list_recycler_view); 51 | noInfectionInformation = view.findViewById(R.id.layout_not_infected1); 52 | recyclerView.setHasFixedSize(true); 53 | layoutManager = new LinearLayoutManager(getActivity()); 54 | recyclerView.setLayoutManager(layoutManager); 55 | mAdapter = new InfectedUUIDsAdapter(); 56 | recyclerView.setAdapter(mAdapter); 57 | cardView = view.findViewById(R.id.infectionCheckFragment); 58 | } 59 | 60 | @Override 61 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 62 | super.onActivityCreated(savedInstanceState); 63 | 64 | mViewModel = ViewModelProviders.of(this).get(InfectionCheckViewModel.class); 65 | mainActivityViewModel = ViewModelProviders.of(getActivity()).get(MainActivityViewModel.class); 66 | 67 | mainActivityViewModel.eventRefresh().observe(getViewLifecycleOwner(), refreshing -> { 68 | if(refreshing) { 69 | mViewModel.refreshInfectedUUIDs(); 70 | } 71 | }); 72 | 73 | mViewModel.getPossiblyInfectedEncounters().observe(getViewLifecycleOwner(), infectedUUIDS -> { 74 | mainActivityViewModel.finishRefresh(); 75 | if(infectedUUIDS.size() != 0) { 76 | mAdapter.setInfectedUUIDs(infectedUUIDS); 77 | noInfectionInformation.setVisibility(View.GONE); 78 | recyclerView.setVisibility(View.VISIBLE); 79 | 80 | cardView.setCardBackgroundColor(getResources().getColor(R.color.colorDanger)); 81 | } 82 | else { 83 | noInfectionInformation.setVisibility(View.VISIBLE); 84 | recyclerView.setVisibility(View.GONE); 85 | cardView.setCardBackgroundColor(getResources().getColor(R.color.colorNoDanger)); 86 | } 87 | }); 88 | 89 | mViewModel.refreshInfectedUUIDs(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/fragments/NearbyDevicesFragment.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.fragments; 2 | 3 | 4 | import android.animation.Animator; 5 | import android.animation.AnimatorListenerAdapter; 6 | import android.animation.ObjectAnimator; 7 | import android.animation.PropertyValuesHolder; 8 | import android.os.Bundle; 9 | import android.os.Handler; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.ImageView; 14 | import android.widget.RelativeLayout; 15 | 16 | import androidx.annotation.NonNull; 17 | import androidx.annotation.Nullable; 18 | import androidx.dynamicanimation.animation.DynamicAnimation; 19 | import androidx.dynamicanimation.animation.SpringAnimation; 20 | import androidx.dynamicanimation.animation.SpringForce; 21 | import androidx.fragment.app.Fragment; 22 | import androidx.lifecycle.ViewModelProviders; 23 | 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | import java.util.Objects; 27 | 28 | import app.bandemic.R; 29 | import app.bandemic.viewmodel.NearbyDevicesViewModel; 30 | 31 | public class NearbyDevicesFragment extends Fragment { 32 | 33 | private RelativeLayout layout; 34 | 35 | private List nearbyDeviceViews = new ArrayList<>(); 36 | public NearbyDevicesViewModel model; 37 | 38 | 39 | @Override 40 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 41 | @Nullable Bundle savedInstanceState) { 42 | return inflater.inflate(R.layout.nearby_devices_fragment, container, false); 43 | } 44 | 45 | @Override 46 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 47 | super.onViewCreated(view, savedInstanceState); 48 | 49 | model = ViewModelProviders.of(this).get(NearbyDevicesViewModel.class); 50 | layout = view.findViewById(R.id.layout); 51 | 52 | updateDevicePositions(Objects.requireNonNull(model.distances.getValue())); 53 | skipAnimations(); 54 | 55 | model.distances.observe(getViewLifecycleOwner(), this::updateDevicePositions); 56 | 57 | ImageView iv = (ImageView) view.findViewById(R.id.nearby_device_myself); 58 | ObjectAnimator pulseAnim = ObjectAnimator.ofPropertyValuesHolder( 59 | iv, 60 | PropertyValuesHolder.ofFloat("scaleX", 1.0f, 1.3f, 1.0f, 1.2f, 1.0f), 61 | PropertyValuesHolder.ofFloat("scaleY", 1.0f, 1.3f, 1.0f, 1.2f, 1.0f)); 62 | pulseAnim.setDuration(600); 63 | Handler handler = new Handler(); 64 | pulseAnim.addListener(new AnimatorListenerAdapter(){ 65 | @Override 66 | public void onAnimationEnd(Animator animation) { 67 | handler.postDelayed(new Runnable() { 68 | @Override 69 | public void run() { 70 | pulseAnim.start(); 71 | } 72 | }, 1500); 73 | } 74 | }); 75 | pulseAnim.start(); 76 | 77 | } 78 | 79 | private class NearbyDeviceView { 80 | ImageView iv; 81 | SpringAnimation animX; 82 | SpringAnimation animY; 83 | } 84 | 85 | private void addNearbyDevice() { 86 | ImageView newIV = new ImageView(this.getContext()); 87 | newIV.setImageDrawable(getResources().getDrawable(R.drawable.nearby_device)); 88 | RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 89 | layoutParams.topMargin = 0; 90 | layoutParams.leftMargin = 0; 91 | layoutParams.addRule(RelativeLayout.ALIGN_LEFT, R.id.nearby_device_myself); 92 | layoutParams.addRule(RelativeLayout.ALIGN_TOP, R.id.nearby_device_myself); 93 | layout.addView(newIV, layoutParams); 94 | 95 | final SpringAnimation anim1X = new SpringAnimation(newIV, 96 | DynamicAnimation.TRANSLATION_X); 97 | final SpringAnimation anim1Y = new SpringAnimation(newIV, 98 | DynamicAnimation.TRANSLATION_Y); 99 | anim1X.setSpring(new SpringForce().setStiffness(SpringForce.STIFFNESS_LOW)); 100 | anim1Y.setSpring(new SpringForce().setStiffness(SpringForce.STIFFNESS_LOW)); 101 | 102 | NearbyDeviceView ndv = new NearbyDeviceView(); 103 | ndv.iv = newIV; 104 | ndv.animX = anim1X; 105 | ndv.animY = anim1Y; 106 | 107 | nearbyDeviceViews.add(ndv); 108 | } 109 | 110 | private void removeNearbyDevice() { 111 | if (nearbyDeviceViews.size() > 0) { 112 | NearbyDeviceView removed = nearbyDeviceViews.remove(0); 113 | removed.animX.cancel(); 114 | removed.animY.cancel(); 115 | layout.removeView(removed.iv); 116 | } 117 | } 118 | 119 | private void updateDevicePositions(double[] distances) { 120 | if (nearbyDeviceViews.size() < distances.length) { 121 | addNearbyDevice(); 122 | } 123 | if (nearbyDeviceViews.size() > distances.length) { 124 | removeNearbyDevice(); 125 | } 126 | 127 | double angle_min = 0f / 180f * Math.PI; 128 | double angle_max = Math.PI - angle_min; 129 | double angle_range = angle_max - angle_min; 130 | 131 | for (int i = 0; i < nearbyDeviceViews.size(); i++) { 132 | double angle = angle_min + (angle_range / (nearbyDeviceViews.size() + 1)) * (i + 1); 133 | double min_distance_on_screen = 100d; 134 | double max_distance_on_screen = 300d; 135 | double distance_scaling = 3d; // Make bigger to make effect of actual distance stronger in near range 136 | double distance = max_distance_on_screen - (1d/((distances[i]/ distance_scaling)+1d) * (max_distance_on_screen - min_distance_on_screen)); 137 | double top = -Math.sin(angle) * distance; 138 | double left = Math.cos(angle) * distance; 139 | NearbyDeviceView ndv = nearbyDeviceViews.get(i); 140 | ndv.animX.animateToFinalPosition((float) left); 141 | ndv.animY.animateToFinalPosition((float) top); 142 | } 143 | } 144 | 145 | public void skipAnimations() { 146 | for (NearbyDeviceView nearbyDeviceView : nearbyDeviceViews) { 147 | nearbyDeviceView.animX.skipToEnd(); 148 | nearbyDeviceView.animY.skipToEnd(); 149 | } 150 | 151 | } 152 | 153 | @Override 154 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 155 | super.onActivityCreated(savedInstanceState); 156 | 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/database/AppDatabase.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.database; 2 | 3 | import android.content.Context; 4 | 5 | import java.util.concurrent.ExecutorService; 6 | import java.util.concurrent.Executors; 7 | 8 | import androidx.room.Database; 9 | import androidx.room.Room; 10 | import androidx.room.RoomDatabase; 11 | import androidx.room.TypeConverters; 12 | 13 | @Database(entities = { 14 | Beacon.class, 15 | OwnUUID.class, 16 | InfectedUUID.class}, 17 | version = 8, exportSchema = false) 18 | @TypeConverters({Converters.class}) 19 | public abstract class AppDatabase extends RoomDatabase { 20 | public abstract BeaconDao beaconDao(); 21 | public abstract OwnUUIDDao ownUUIDDao(); 22 | public abstract InfectedUUIDDao infectedUUIDDao(); 23 | 24 | private static volatile AppDatabase INSTANCE; 25 | private static final int NUMBER_OF_THREADS = 4; 26 | public static final ExecutorService databaseWriteExecutor = 27 | Executors.newFixedThreadPool(NUMBER_OF_THREADS); 28 | 29 | public static AppDatabase getDatabase(final Context context) { 30 | if(INSTANCE == null) { 31 | synchronized (AppDatabase.class) { 32 | INSTANCE = Room.databaseBuilder(context.getApplicationContext(), 33 | AppDatabase.class, "bandemic_database").fallbackToDestructiveMigration().build(); 34 | } 35 | } 36 | return INSTANCE; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/database/Beacon.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.database; 2 | 3 | import android.util.Log; 4 | 5 | import java.security.MessageDigest; 6 | import java.security.NoSuchAlgorithmException; 7 | import java.util.Date; 8 | 9 | import androidx.room.Entity; 10 | import androidx.room.PrimaryKey; 11 | 12 | @Entity 13 | public class Beacon { 14 | private static final String LOG_TAG = "Beacon"; 15 | 16 | @PrimaryKey(autoGenerate = true) 17 | public int id = 0; 18 | public byte[] receivedHash; 19 | public byte[] receivedDoubleHash; 20 | public Date timestamp; 21 | public double distance; 22 | public long duration; 23 | 24 | public Beacon(byte[] receivedHash, Date timestamp, long duration, double distance) { 25 | this.receivedHash = receivedHash; 26 | MessageDigest digest; 27 | try { 28 | digest = MessageDigest.getInstance("SHA-256"); 29 | } catch (NoSuchAlgorithmException e) { 30 | Log.wtf(LOG_TAG, e); 31 | throw new RuntimeException(e); 32 | } 33 | this.receivedDoubleHash = digest.digest(receivedHash); 34 | this.timestamp = timestamp; 35 | this.duration = duration; 36 | this.distance = distance; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/database/BeaconDao.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.database; 2 | 3 | import java.util.List; 4 | 5 | import androidx.lifecycle.LiveData; 6 | import androidx.room.Dao; 7 | import androidx.room.Delete; 8 | import androidx.room.Insert; 9 | import androidx.room.OnConflictStrategy; 10 | import androidx.room.Query; 11 | 12 | @Dao 13 | public interface BeaconDao { 14 | @Query("SELECT * FROM beacon") 15 | LiveData> getAll(); 16 | 17 | @Query("SELECT * FROM beacon") 18 | LiveData> getAllDistinctBroadcast(); 19 | 20 | /*@Query("SELECT * FROM user WHERE uid IN (:userIds)") 21 | List loadAllByIds(int[] userIds); 22 | 23 | @Query("SELECT * FROM user WHERE first_name LIKE :first AND " + 24 | "last_name LIKE :last LIMIT 1") 25 | User findByName(String first, String last);*/ 26 | 27 | @Insert(onConflict = OnConflictStrategy.REPLACE) 28 | void insertAll(Beacon... beacons); 29 | 30 | @Delete 31 | void delete(Beacon beacon); 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/database/Converters.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.database; 2 | 3 | import java.util.Date; 4 | import java.util.UUID; 5 | 6 | import androidx.room.TypeConverter; 7 | 8 | public class Converters { 9 | 10 | @TypeConverter 11 | public static UUID fromStringUUID(String value) { 12 | return UUID.fromString(value); 13 | } 14 | 15 | @TypeConverter 16 | public static String toStringUUID(UUID uuid) { 17 | return uuid.toString(); 18 | } 19 | 20 | @TypeConverter 21 | public static Date fromTimestamp(Long value) { 22 | return value == null ? null : new Date(value); 23 | } 24 | 25 | @TypeConverter 26 | public static Long dateToTimestamp(Date date) { 27 | return date == null ? null : date.getTime(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/database/InfectedUUID.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.database; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.util.Date; 6 | 7 | import androidx.room.Entity; 8 | import androidx.room.PrimaryKey; 9 | 10 | @Entity 11 | public class InfectedUUID { 12 | @PrimaryKey 13 | @SerializedName("id") 14 | public int id; 15 | @SerializedName("created_on") 16 | public Date createdOn; 17 | @SerializedName("distrust_level") 18 | public int distrustLevel; 19 | @SerializedName("hashed_id") 20 | public byte[] hashedId; // this is actually double hashed 21 | @SerializedName("icd_code") 22 | public String icdCode; 23 | 24 | public InfectedUUID( 25 | int id, 26 | Date createdOn, 27 | int distrustLevel, 28 | byte[] hashedId, 29 | String icdCode 30 | ) { 31 | this.id = id; 32 | this.createdOn = createdOn; 33 | this.distrustLevel = distrustLevel; 34 | this.hashedId = hashedId; 35 | this.icdCode = icdCode; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/database/InfectedUUIDDao.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.database; 2 | 3 | import java.util.List; 4 | 5 | import androidx.lifecycle.LiveData; 6 | import androidx.room.Dao; 7 | import androidx.room.Delete; 8 | import androidx.room.Insert; 9 | import androidx.room.OnConflictStrategy; 10 | import androidx.room.Query; 11 | 12 | @Dao 13 | public interface InfectedUUIDDao { 14 | @Query("SELECT * FROM infecteduuid") 15 | LiveData> getAll(); 16 | 17 | /*@Query("SELECT * FROM user WHERE uid IN (:userIds)") 18 | List loadAllByIds(int[] userIds); 19 | 20 | @Query("SELECT * FROM user WHERE first_name LIKE :first AND " + 21 | "last_name LIKE :last LIMIT 1") 22 | User findByName(String first, String last);*/ 23 | 24 | // TODO: change != to =, this is just for demo!!!! 25 | @Query("SELECT infecteduuid.id, beacon.timestamp, distance, createdOn, distrustLevel, icdCode" + 26 | " FROM infecteduuid JOIN beacon ON" + 27 | " infecteduuid.hashedId != beacon.receivedDoubleHash") 28 | LiveData> getPossiblyInfectedEncounters(); 29 | 30 | @Insert(onConflict = OnConflictStrategy.REPLACE) 31 | void insertAll(InfectedUUID... uuids); 32 | 33 | @Delete 34 | void delete(InfectedUUID infectedUUID); 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/database/Infection.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.database; 2 | 3 | import java.util.Date; 4 | 5 | import androidx.room.ColumnInfo; 6 | 7 | public class Infection { 8 | @ColumnInfo(name = "id") 9 | public int infectionId; 10 | @ColumnInfo(name = "timestamp") 11 | public Date encounterDate; 12 | public double distance; 13 | public Date createdOn; 14 | public int distrustLevel; 15 | public String icdCode; 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/database/OwnUUID.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.database; 2 | 3 | import java.util.Date; 4 | import java.util.UUID; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.room.Entity; 8 | import androidx.room.PrimaryKey; 9 | 10 | @Entity 11 | public class OwnUUID { 12 | @NonNull 13 | @PrimaryKey 14 | public UUID ownUUID; 15 | public Date timestamp; 16 | 17 | public OwnUUID(@NonNull UUID ownUUID, Date timestamp) { 18 | this.ownUUID = ownUUID; 19 | this.timestamp = timestamp; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/database/OwnUUIDDao.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.database; 2 | 3 | import java.util.List; 4 | 5 | import androidx.lifecycle.LiveData; 6 | import androidx.room.Dao; 7 | import androidx.room.Delete; 8 | import androidx.room.Insert; 9 | import androidx.room.Query; 10 | 11 | @Dao 12 | public interface OwnUUIDDao { 13 | @Query("SELECT * FROM ownuuid") 14 | LiveData> getAll(); 15 | 16 | /*@Query("SELECT * FROM user WHERE uid IN (:userIds)") 17 | List loadAllByIds(int[] userIds); 18 | 19 | @Query("SELECT * FROM user WHERE first_name LIKE :first AND " + 20 | "last_name LIKE :last LIMIT 1") 21 | User findByName(String first, String last);*/ 22 | 23 | @Insert 24 | void insertAll(OwnUUID... uuids); 25 | 26 | @Delete 27 | void delete(OwnUUID ownUUID); 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/network/HexStringToByteArrayTypeAdapter.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.network; 2 | 3 | import com.google.gson.JsonDeserializationContext; 4 | import com.google.gson.JsonDeserializer; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonParseException; 7 | import com.google.gson.JsonPrimitive; 8 | import com.google.gson.JsonSerializationContext; 9 | import com.google.gson.JsonSerializer; 10 | 11 | import org.apache.commons.codec.DecoderException; 12 | import org.apache.commons.codec.binary.Hex; 13 | 14 | import java.lang.reflect.Type; 15 | 16 | public class HexStringToByteArrayTypeAdapter implements JsonSerializer, JsonDeserializer { 17 | @Override 18 | public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 19 | try { 20 | return Hex.decodeHex(json.getAsString().toCharArray()); 21 | } catch (DecoderException e) { 22 | throw new JsonParseException(e.getMessage()); 23 | } 24 | } 25 | 26 | @Override 27 | public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) { 28 | return new JsonPrimitive(String.valueOf(Hex.encodeHex(src))); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/network/InfectedUUIDResponse.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.network; 2 | 3 | import app.bandemic.strict.database.InfectedUUID; 4 | 5 | import java.util.List; 6 | 7 | public class InfectedUUIDResponse { 8 | public List data; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/network/InfectionchainWebservice.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.network; 2 | 3 | import retrofit2.Call; 4 | import retrofit2.http.GET; 5 | 6 | public interface InfectionchainWebservice { 7 | 8 | @GET("strict/items/infected_ids") 9 | Call getInfectedUUIDResponse(); 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/network/RetrofitClient.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.network; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | 6 | import retrofit2.Retrofit; 7 | import retrofit2.converter.gson.GsonConverterFactory; 8 | 9 | public class RetrofitClient { 10 | 11 | private static Retrofit retrofit = null; 12 | private static InfectionchainWebservice webservice = null; 13 | public static final String BASE_URL = "https://backend.infectionchain.online"; 14 | 15 | // should this be synchronized? 16 | public static InfectionchainWebservice getInfectionchainWebservice() { 17 | if(retrofit == null || webservice == null) { 18 | Gson gson = new GsonBuilder() 19 | .setDateFormat("yyyy-MM-dd HH:mm:ss") 20 | .registerTypeHierarchyAdapter(byte[].class, new HexStringToByteArrayTypeAdapter()) 21 | .create(); 22 | 23 | retrofit = new Retrofit.Builder() 24 | .baseUrl(BASE_URL) 25 | .addConverterFactory(GsonConverterFactory.create(gson)) 26 | .build(); 27 | webservice = retrofit.create(InfectionchainWebservice.class); 28 | } 29 | return webservice; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/repository/BroadcastRepository.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.repository; 2 | 3 | import android.app.Application; 4 | 5 | import app.bandemic.strict.database.AppDatabase; 6 | import app.bandemic.strict.database.Beacon; 7 | import app.bandemic.strict.database.BeaconDao; 8 | import app.bandemic.strict.database.OwnUUID; 9 | import app.bandemic.strict.database.OwnUUIDDao; 10 | 11 | import java.util.List; 12 | 13 | import androidx.lifecycle.LiveData; 14 | 15 | public class BroadcastRepository { 16 | 17 | private OwnUUIDDao mOwnUUIDDao; 18 | private BeaconDao mBeaconDao; 19 | private LiveData> mAllOwnUUIDs; 20 | private LiveData> mAllBeacons; 21 | private LiveData> mDistinctBeacons; 22 | 23 | public BroadcastRepository(Application application) { 24 | AppDatabase db = AppDatabase.getDatabase(application); 25 | mOwnUUIDDao = db.ownUUIDDao(); 26 | mBeaconDao = db.beaconDao(); 27 | mAllOwnUUIDs = mOwnUUIDDao.getAll(); 28 | mAllBeacons = mBeaconDao.getAll(); 29 | mDistinctBeacons = mBeaconDao.getAllDistinctBroadcast(); 30 | } 31 | 32 | public LiveData> getAllOwnUUIDs() { 33 | return mAllOwnUUIDs; 34 | } 35 | 36 | public LiveData> getAllBeacons() { 37 | return mAllBeacons; 38 | } 39 | 40 | public LiveData> getDistinctBeacons() { 41 | return mDistinctBeacons; 42 | } 43 | 44 | public void insertOwnUUID(OwnUUID ownUUID) { 45 | AppDatabase.databaseWriteExecutor.execute(() -> { 46 | mOwnUUIDDao.insertAll(ownUUID); 47 | }); 48 | } 49 | 50 | public void insertBeacon(Beacon beacon) { 51 | AppDatabase.databaseWriteExecutor.execute(() -> { 52 | mBeaconDao.insertAll(beacon); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/repository/InfectedUUIDRepository.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.repository; 2 | 3 | import android.app.Application; 4 | import android.util.Log; 5 | 6 | import app.bandemic.strict.database.AppDatabase; 7 | import app.bandemic.strict.database.InfectedUUID; 8 | import app.bandemic.strict.database.InfectedUUIDDao; 9 | import app.bandemic.strict.database.Infection; 10 | import app.bandemic.strict.network.InfectedUUIDResponse; 11 | import app.bandemic.strict.network.InfectionchainWebservice; 12 | import app.bandemic.strict.network.RetrofitClient; 13 | 14 | import java.util.List; 15 | 16 | import androidx.lifecycle.LiveData; 17 | import retrofit2.Call; 18 | import retrofit2.Callback; 19 | import retrofit2.Response; 20 | 21 | public class InfectedUUIDRepository { 22 | 23 | private static final String LOG_TAG = "InfectedUUIDRepository"; 24 | 25 | private InfectionchainWebservice webservice; 26 | 27 | private InfectedUUIDDao infectedUUIDDao; 28 | 29 | public InfectedUUIDRepository(Application application) { 30 | webservice = RetrofitClient.getInfectionchainWebservice(); 31 | AppDatabase db = AppDatabase.getDatabase(application); 32 | infectedUUIDDao = db.infectedUUIDDao(); 33 | } 34 | 35 | public LiveData> getInfectedUUIDs() { 36 | refreshInfectedUUIDs(); 37 | return infectedUUIDDao.getAll(); 38 | } 39 | 40 | public LiveData> getPossiblyInfectedEncounters() { 41 | return infectedUUIDDao.getPossiblyInfectedEncounters(); 42 | } 43 | 44 | public void refreshInfectedUUIDs() { 45 | webservice.getInfectedUUIDResponse().enqueue(new Callback() { 46 | @Override 47 | public void onResponse(Call call, Response response) { 48 | AppDatabase.databaseWriteExecutor.execute(() -> { 49 | if(response.body() != null) { 50 | infectedUUIDDao.insertAll(response.body().data.toArray(new InfectedUUID[response.body().data.size()])); 51 | } 52 | else { 53 | // TODO: error handling! 54 | Log.e(LOG_TAG, "Invalid response from api"); 55 | } 56 | 57 | }); 58 | } 59 | 60 | @Override 61 | public void onFailure(Call call, Throwable t) { 62 | // TODO error handling 63 | //Log.e(LOG_TAG, t.getCause().getMessage()); 64 | //Log.e(LOG_TAG, t.getMessage() + t.getStackTrace().toString()); 65 | } 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/service/BandemicProfile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package app.bandemic.strict.service; 18 | 19 | import android.bluetooth.BluetoothGattCharacteristic; 20 | import android.bluetooth.BluetoothGattService; 21 | 22 | import java.util.UUID; 23 | 24 | /** 25 | * Implementation of the Bluetooth GATT Time Profile. 26 | * https://www.bluetooth.com/specifications/adopted-specifications 27 | */ 28 | public class BandemicProfile { 29 | private static final String TAG = BandemicProfile.class.getSimpleName(); 30 | 31 | /* Current Bandemic Service UUID */ 32 | public static UUID BANDEMIC_SERVICE = UUID.fromString("cc84f3ec-7d80-4a58-b15e-9192adf7d6e4"); 33 | /* Mandatory Hash of UUID Characteristic */ 34 | public static UUID HASH_OF_UUID = UUID.fromString("db10ab98-107c-4b7b-a9c5-26fe559d82b4"); 35 | 36 | /** 37 | * Return a configured {@link BluetoothGattService} instance for the 38 | * Current Time Service. 39 | */ 40 | public static BluetoothGattService createBandemicService() { 41 | BluetoothGattService service = new BluetoothGattService(BANDEMIC_SERVICE, 42 | BluetoothGattService.SERVICE_TYPE_PRIMARY); 43 | 44 | BluetoothGattCharacteristic hashOfUUID = createHashOfUUIDCharacteristic(); 45 | service.addCharacteristic(hashOfUUID); 46 | 47 | return service; 48 | } 49 | 50 | public static BluetoothGattCharacteristic createHashOfUUIDCharacteristic() { 51 | return new BluetoothGattCharacteristic(HASH_OF_UUID, 52 | BluetoothGattCharacteristic.PROPERTY_READ, 53 | BluetoothGattCharacteristic.PERMISSION_READ); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/service/BeaconCache.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.service; 2 | 3 | import android.os.Handler; 4 | import android.util.Log; 5 | 6 | import androidx.collection.CircularArray; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Date; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | 13 | import app.bandemic.strict.database.Beacon; 14 | import app.bandemic.strict.repository.BroadcastRepository; 15 | import okio.ByteString; 16 | 17 | public class BeaconCache { 18 | private static final String LOG_TAG = "BeaconCache"; 19 | private final int MOVING_AVERAGE_LENGTH = 7; 20 | private final long FLUSH_AFTER_MILLIS = 1000 * 60 * 3; // flush after three minutes 21 | 22 | private BroadcastRepository broadcastRepository; 23 | private Handler serviceHandler; 24 | private HashMap cache = new HashMap<>(); 25 | 26 | public List nearbyDevicesListeners = new ArrayList<>(); 27 | 28 | public interface NearbyDevicesListener { 29 | void onNearbyDevicesChanged(double[] distances); 30 | } 31 | 32 | public BeaconCache(BroadcastRepository broadcastRepository, Handler serviceHandler) { 33 | this.broadcastRepository = broadcastRepository; 34 | this.serviceHandler = serviceHandler; 35 | } 36 | 37 | private void flush(ByteString hash) { 38 | Log.d(LOG_TAG, "Flushing distance to DB"); 39 | CacheEntry entry = cache.get(hash); 40 | insertIntoDB(entry.hash, entry.lowestDistance, entry.firstReceived, entry.lastReceived - entry.firstReceived); 41 | cache.remove(hash); 42 | } 43 | 44 | public void flush() { 45 | for (ByteString hash : cache.keySet()) { 46 | flush(hash); 47 | } 48 | } 49 | 50 | public void addReceivedBroadcast(byte[] hash, double distance) { 51 | ByteString hashString = ByteString.of(hash); 52 | CacheEntry entry = cache.get(hashString); 53 | 54 | if (entry == null) { 55 | // new unknown broadcast 56 | entry = new CacheEntry(); 57 | cache.put(hashString, entry); 58 | entry.hash = hash; 59 | entry.firstReceived = System.currentTimeMillis(); 60 | } 61 | 62 | entry.lastReceived = System.currentTimeMillis(); 63 | 64 | // postpone flushing 65 | serviceHandler.removeCallbacks(entry.flushRunnable); 66 | serviceHandler.postDelayed(entry.flushRunnable, FLUSH_AFTER_MILLIS); 67 | 68 | CircularArray distances = entry.distances; 69 | distances.addFirst(distance); 70 | if (distances.size() == MOVING_AVERAGE_LENGTH) { 71 | 72 | //calculate moving average 73 | double avg = entry.getAverageDistance(); 74 | if (avg < entry.lowestDistance) { 75 | //insert new lowest value to DB 76 | entry.lowestDistance = avg; 77 | //insertIntoDB(hash, avg); 78 | } 79 | distances.popLast(); 80 | } 81 | 82 | sendNearbyDevices(); 83 | 84 | } 85 | 86 | private void sendNearbyDevices() { 87 | double[] nearbyDevices = getNearbyDevices(); 88 | 89 | for (NearbyDevicesListener listener : nearbyDevicesListeners) { 90 | listener.onNearbyDevicesChanged(nearbyDevices); 91 | } 92 | } 93 | 94 | public double[] getNearbyDevices() { 95 | double[] nearbyDevices = new double[cache.size()]; 96 | int i = 0; 97 | for (CacheEntry entry : cache.values()) { 98 | nearbyDevices[i] = entry.getAverageDistance(); 99 | i++; 100 | } 101 | return nearbyDevices; 102 | } 103 | 104 | private void insertIntoDB(byte[] hash, double distance, long startTime, long duration) { 105 | broadcastRepository.insertBeacon(new Beacon( 106 | hash, 107 | new Date(startTime), 108 | duration, 109 | distance 110 | )); 111 | } 112 | 113 | private class CacheEntry { 114 | long firstReceived; 115 | long lastReceived; 116 | byte[] hash; 117 | CircularArray distances = new CircularArray<>(MOVING_AVERAGE_LENGTH); 118 | double lowestDistance = Double.MAX_VALUE; 119 | Runnable flushRunnable = () -> flush(ByteString.of(hash)); 120 | 121 | double getAverageDistance() { 122 | double avg = 0; 123 | for (int i = 0; i < distances.size(); i++) { 124 | avg += distances.get(i) / distances.size(); 125 | } 126 | return avg; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/service/BleAdvertiser.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.service; 2 | 3 | import android.bluetooth.BluetoothAdapter; 4 | import android.bluetooth.BluetoothDevice; 5 | import android.bluetooth.BluetoothGatt; 6 | import android.bluetooth.BluetoothGattCharacteristic; 7 | import android.bluetooth.BluetoothGattDescriptor; 8 | import android.bluetooth.BluetoothGattServer; 9 | import android.bluetooth.BluetoothGattServerCallback; 10 | import android.bluetooth.BluetoothManager; 11 | import android.bluetooth.BluetoothProfile; 12 | import android.bluetooth.le.AdvertiseCallback; 13 | import android.bluetooth.le.AdvertiseData; 14 | import android.bluetooth.le.AdvertiseSettings; 15 | import android.bluetooth.le.BluetoothLeAdvertiser; 16 | import android.content.Context; 17 | import android.os.ParcelUuid; 18 | import android.util.Log; 19 | 20 | import java.util.Arrays; 21 | 22 | public class BleAdvertiser { 23 | private static final String LOG_TAG = "BleAdvertiser"; 24 | private final Context context; 25 | 26 | private byte[] broadcastData; 27 | private BluetoothLeAdvertiser bluetoothLeAdvertiser; 28 | private AdvertiseCallback bluetoothAdvertiseCallback; 29 | private BluetoothGattServer mBluetoothGattServer; 30 | private BluetoothManager bluetoothManager; 31 | private final BluetoothAdapter bluetoothAdapter; 32 | 33 | public BleAdvertiser(BluetoothManager bluetoothManager, Context context) { 34 | bluetoothAdapter = bluetoothManager.getAdapter(); 35 | this.bluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser(); 36 | this.bluetoothManager = bluetoothManager; 37 | this.context = context; 38 | } 39 | 40 | public void setBroadcastData(byte[] broadcastData) { 41 | this.broadcastData = broadcastData; 42 | 43 | //Restart advertising so that mac address changes 44 | //Otherwise the device could be recognized that way even though the UUID has changed 45 | if(bluetoothAdvertiseCallback != null) { 46 | restartAdvertising(); 47 | } 48 | } 49 | 50 | private void restartAdvertising() { 51 | stopAdvertising(); 52 | startAdvertising(); 53 | } 54 | 55 | public void startAdvertising() { 56 | Log.i(LOG_TAG, "Starting Advertising"); 57 | AdvertiseSettings settings = new AdvertiseSettings.Builder() 58 | .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) 59 | .setConnectable(true) 60 | .setTimeout(0) 61 | .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) 62 | .build(); 63 | 64 | AdvertiseData data = new AdvertiseData.Builder() 65 | .setIncludeTxPowerLevel(true) 66 | .addServiceUuid(new ParcelUuid(BandemicProfile.BANDEMIC_SERVICE)) 67 | .setIncludeDeviceName(false) 68 | .build(); 69 | 70 | bluetoothAdvertiseCallback = new AdvertiseCallback() { 71 | @Override 72 | public void onStartSuccess(AdvertiseSettings settingsInEffect) { 73 | super.onStartSuccess(settingsInEffect); 74 | Log.i(LOG_TAG, "Advertising onStartSuccess"); 75 | } 76 | 77 | @Override 78 | public void onStartFailure(int errorCode) { 79 | super.onStartFailure(errorCode); 80 | Log.e(LOG_TAG, "Advertising onStartFailure: " + errorCode); 81 | // TODO 82 | } 83 | }; 84 | 85 | 86 | //Set fixed device name to avoid being recognizable except though UUID 87 | //Name is not sent in advertisement anyway but can be requested though GATT Server 88 | //and I don't see any way to disable that 89 | bluetoothAdapter.setName("Phone"); 90 | 91 | // TODO: check if null when launching with Bluetooth disabled 92 | bluetoothLeAdvertiser.startAdvertising(settings, data, bluetoothAdvertiseCallback); 93 | 94 | 95 | mBluetoothGattServer = bluetoothManager.openGattServer(context, mGattServerCallback); 96 | if (mBluetoothGattServer == null) { 97 | Log.w(LOG_TAG, "Unable to create GATT server"); 98 | return; 99 | } 100 | 101 | mBluetoothGattServer.addService(BandemicProfile.createBandemicService()); 102 | } 103 | 104 | public void stopAdvertising() { 105 | Log.d(LOG_TAG, "Stopping advertising"); 106 | if (bluetoothAdvertiseCallback != null) { 107 | bluetoothLeAdvertiser.stopAdvertising(bluetoothAdvertiseCallback); 108 | bluetoothAdvertiseCallback = null; 109 | } 110 | if (mBluetoothGattServer != null) { 111 | mBluetoothGattServer.close(); 112 | mBluetoothGattServer = null; 113 | } 114 | } 115 | 116 | private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() { 117 | 118 | @Override 119 | public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { 120 | if (newState == BluetoothProfile.STATE_CONNECTED) { 121 | Log.i(LOG_TAG, "BluetoothDevice CONNECTED: " + device); 122 | } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { 123 | Log.i(LOG_TAG, "BluetoothDevice DISCONNECTED: " + device); 124 | } 125 | } 126 | 127 | @Override 128 | public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, 129 | BluetoothGattCharacteristic characteristic) { 130 | if (BandemicProfile.HASH_OF_UUID.equals(characteristic.getUuid())) { 131 | Log.i(LOG_TAG, "Read Hash of UUID"); 132 | Log.i(LOG_TAG, "Offset: " + offset); 133 | mBluetoothGattServer.sendResponse(device, 134 | requestId, 135 | BluetoothGatt.GATT_SUCCESS, 136 | offset, 137 | Arrays.copyOfRange(broadcastData, offset, broadcastData.length)); 138 | } else { 139 | // Invalid characteristic 140 | Log.w(LOG_TAG, "Invalid Characteristic Read: " + characteristic.getUuid()); 141 | mBluetoothGattServer.sendResponse(device, 142 | requestId, 143 | BluetoothGatt.GATT_FAILURE, 144 | 0, 145 | null); 146 | } 147 | } 148 | 149 | @Override 150 | public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, 151 | BluetoothGattDescriptor descriptor) { 152 | Log.w(LOG_TAG, "Unknown descriptor read request"); 153 | mBluetoothGattServer.sendResponse(device, 154 | requestId, 155 | BluetoothGatt.GATT_FAILURE, 156 | 0, 157 | null); 158 | } 159 | 160 | @Override 161 | public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, 162 | BluetoothGattDescriptor descriptor, 163 | boolean preparedWrite, boolean responseNeeded, 164 | int offset, byte[] value) { 165 | Log.w(LOG_TAG, "Unknown descriptor write request"); 166 | if (responseNeeded) { 167 | mBluetoothGattServer.sendResponse(device, 168 | requestId, 169 | BluetoothGatt.GATT_FAILURE, 170 | 0, 171 | null); 172 | } 173 | } 174 | }; 175 | } 176 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/service/BleScanner.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.service; 2 | 3 | import android.bluetooth.BluetoothAdapter; 4 | import android.bluetooth.BluetoothDevice; 5 | import android.bluetooth.BluetoothGatt; 6 | import android.bluetooth.BluetoothGattCallback; 7 | import android.bluetooth.BluetoothGattCharacteristic; 8 | import android.bluetooth.BluetoothGattService; 9 | import android.bluetooth.le.BluetoothLeScanner; 10 | import android.bluetooth.le.ScanCallback; 11 | import android.bluetooth.le.ScanFilter; 12 | import android.bluetooth.le.ScanRecord; 13 | import android.bluetooth.le.ScanResult; 14 | import android.bluetooth.le.ScanSettings; 15 | import android.content.Context; 16 | import android.os.Build; 17 | import android.os.ParcelUuid; 18 | import android.os.SystemClock; 19 | import android.util.Log; 20 | import android.util.LruCache; 21 | 22 | import java.util.Collections; 23 | 24 | public class BleScanner { 25 | private static final String LOG_TAG = "BleScanner"; 26 | private final BluetoothLeScanner bluetoothLeScanner; 27 | private BluetoothAdapter bluetoothAdapter; 28 | private final BeaconCache beaconCache; 29 | private final Context context; 30 | 31 | private ScanCallback bluetoothScanCallback; 32 | 33 | // Cache the hash of uuid returned by the device so we don't have to connect again every time 34 | private LruCache macAddressCache = new LruCache<>(100); 35 | 36 | // Remember start time of connections so we don't start multiple connections per device 37 | private LruCache connStartedTimeMap = new LruCache<>(100); 38 | 39 | public BleScanner(BluetoothAdapter bluetoothAdapter, BeaconCache beaconCache, Context context) { 40 | this.bluetoothAdapter = bluetoothAdapter; 41 | this.bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); 42 | this.beaconCache = beaconCache; 43 | this.context = context; 44 | } 45 | 46 | private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); 47 | private static String bytesToHex(byte[] bytes) { 48 | char[] hexChars = new char[bytes.length * 2]; 49 | for (int j = 0; j < bytes.length; j++) { 50 | int v = bytes[j] & 0xFF; 51 | hexChars[j * 2] = HEX_ARRAY[v >>> 4]; 52 | hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; 53 | } 54 | return new String(hexChars); 55 | } 56 | 57 | public void startScanning() { 58 | Log.d(LOG_TAG, "Starting scan"); 59 | 60 | bluetoothScanCallback = new ScanCallback() { 61 | public void onScanResult(int callbackType, ScanResult result) { 62 | 63 | Log.d(LOG_TAG, "onScanResult"); 64 | 65 | ScanRecord record = result.getScanRecord(); 66 | 67 | // if there is no record, discard this packet 68 | if (record == null) { 69 | return; 70 | } 71 | 72 | //TODO The values here seem wrong 73 | int txPower = -65;//record.getTxPowerLevel(); 74 | int rssi = result.getRssi(); 75 | 76 | BluetoothDevice device = result.getDevice(); 77 | 78 | String deviceAddress = device.getAddress(); 79 | 80 | Log.i(LOG_TAG, "Found device with rssi=" + rssi + " txPower="+txPower); 81 | 82 | 83 | double distance = Math.pow(10d, ((double) txPower - rssi) / (10 * 2)); 84 | 85 | //Cache UUID results for mac addresses we have already connected to 86 | // so we don't have to build up connections again. 87 | //Only send updated distance to BeaconCache 88 | byte[] hashOfUUIDCached = macAddressCache.get(deviceAddress); 89 | if (hashOfUUIDCached != null) { 90 | Log.i(LOG_TAG, "Address seen already: " + deviceAddress + " New distance: " + distance); 91 | beaconCache.addReceivedBroadcast(hashOfUUIDCached, distance); 92 | } else { 93 | 94 | //Only start connection for the same device max every 5 sec so that we don't 95 | // have multiple connections for the same device running 96 | Long connStartedTime = connStartedTimeMap.get(deviceAddress); 97 | if (connStartedTime == null || (SystemClock.elapsedRealtime() - connStartedTime) > 5000) { 98 | connStartedTimeMap.put(deviceAddress, SystemClock.elapsedRealtime()); 99 | 100 | Log.i(LOG_TAG, "Address not seen yet: " + deviceAddress + " Distance: " + distance); 101 | BluetoothGattCallback gattCallback = new BluetoothGattCallback() { 102 | @Override 103 | public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { 104 | Log.i(LOG_TAG, "State changed to " + newState); 105 | if (newState == BluetoothGatt.STATE_CONNECTED) { 106 | gatt.discoverServices(); 107 | 108 | } 109 | } 110 | 111 | @Override 112 | public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { 113 | Log.i(LOG_TAG, "Characteristic read " + characteristic.getUuid()); 114 | if (characteristic.getUuid().compareTo(BandemicProfile.HASH_OF_UUID) == 0) { 115 | byte[] hashOfUUID = characteristic.getValue(); 116 | Log.i(LOG_TAG, "Read hash of uuid characteristic: " + bytesToHex(hashOfUUID)); 117 | macAddressCache.put(deviceAddress, hashOfUUID); 118 | beaconCache.addReceivedBroadcast(hashOfUUID, distance); 119 | gatt.close(); 120 | } 121 | } 122 | 123 | @Override 124 | public void onServicesDiscovered(BluetoothGatt gatt, int status) { 125 | Log.i(LOG_TAG, "Services Discovered"); 126 | BluetoothGattCharacteristic characteristic = gatt.getService(BandemicProfile.BANDEMIC_SERVICE).getCharacteristic(BandemicProfile.HASH_OF_UUID); 127 | if (characteristic != null) { 128 | Log.i(LOG_TAG, "Read characteristic..."); 129 | gatt.readCharacteristic(characteristic); 130 | } else { 131 | Log.i(LOG_TAG, "============================="); 132 | Log.e(LOG_TAG, "Did not find expected characteristic"); 133 | Log.i(LOG_TAG, "Found these instead:"); 134 | for (BluetoothGattService service : gatt.getServices()) { 135 | Log.i(LOG_TAG, "Service: " + service.getUuid()); 136 | for (BluetoothGattCharacteristic gattCharacteristic : service.getCharacteristics()) { 137 | Log.i(LOG_TAG, " Characteristic: " + gattCharacteristic.getUuid()); 138 | } 139 | } 140 | Log.i(LOG_TAG, "============================="); 141 | gatt.close(); 142 | } 143 | } 144 | }; 145 | device.connectGatt(context, false, gattCallback); 146 | } 147 | } 148 | 149 | } 150 | }; 151 | 152 | ScanFilter filter = new ScanFilter.Builder() 153 | .setServiceUuid(new ParcelUuid(BandemicProfile.BANDEMIC_SERVICE)) 154 | .build(); 155 | 156 | ScanSettings.Builder settingsBuilder = new ScanSettings.Builder() 157 | .setScanMode(ScanSettings.SCAN_MODE_BALANCED) 158 | .setReportDelay(0); 159 | 160 | 161 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 162 | settingsBuilder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) 163 | .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT) 164 | .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE); 165 | } 166 | 167 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 168 | settingsBuilder.setLegacy(true); 169 | } 170 | 171 | bluetoothLeScanner.startScan(Collections.singletonList(filter), settingsBuilder.build(), bluetoothScanCallback); 172 | } 173 | 174 | public void stopScanning() { 175 | Log.d(LOG_TAG, "Stopping scanning"); 176 | 177 | if (bluetoothScanCallback != null && bluetoothAdapter.isEnabled()) { 178 | bluetoothLeScanner.stopScan(bluetoothScanCallback); 179 | } 180 | 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/service/StartupListener.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.service; 2 | 3 | import android.Manifest; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | 9 | import androidx.core.content.ContextCompat; 10 | 11 | /* 12 | This BroadcastReceiver starts the tracing service when the system boots 13 | */ 14 | public class StartupListener extends BroadcastReceiver { 15 | @Override 16 | public void onReceive(Context context, Intent intent) { 17 | if(Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) 18 | { 19 | if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) 20 | != PackageManager.PERMISSION_GRANTED) { 21 | Intent serviceIntent = new Intent(context, TracingService.class); 22 | context.startService(serviceIntent); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/strict/service/TracingService.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.strict.service; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Notification; 5 | import android.app.NotificationChannel; 6 | import android.app.NotificationManager; 7 | import android.app.PendingIntent; 8 | import android.app.Service; 9 | import android.bluetooth.BluetoothAdapter; 10 | import android.bluetooth.BluetoothManager; 11 | import android.content.BroadcastReceiver; 12 | import android.content.Context; 13 | import android.content.Intent; 14 | import android.content.IntentFilter; 15 | import android.graphics.Color; 16 | import android.location.LocationManager; 17 | import android.location.LocationProvider; 18 | import android.os.Binder; 19 | import android.os.Build; 20 | import android.os.Handler; 21 | import android.os.HandlerThread; 22 | import android.os.IBinder; 23 | import android.os.Looper; 24 | import android.util.Log; 25 | 26 | import androidx.core.app.NotificationCompat; 27 | 28 | import java.nio.ByteBuffer; 29 | import java.security.MessageDigest; 30 | import java.security.NoSuchAlgorithmException; 31 | import java.util.ArrayList; 32 | import java.util.Arrays; 33 | import java.util.Date; 34 | import java.util.List; 35 | import java.util.UUID; 36 | 37 | import app.bandemic.R; 38 | import app.bandemic.strict.database.OwnUUID; 39 | import app.bandemic.strict.repository.BroadcastRepository; 40 | import app.bandemic.ui.MainActivity; 41 | 42 | public class TracingService extends Service { 43 | private static final String LOG_TAG = "TracingService"; 44 | private static final String DEFAULT_NOTIFICATION_CHANNEL = "ContactTracing"; 45 | private static final int NOTIFICATION_ID = 1; 46 | 47 | public static final int BLUETOOTH_SIG = 2220; 48 | public static final int HASH_LENGTH = 26; 49 | public static final int BROADCAST_LENGTH = HASH_LENGTH + 1; 50 | private static final int UUID_VALID_TIME = 1000 * 60 * 30; //ms * sec * 30 min 51 | 52 | private Looper serviceLooper; 53 | private Handler serviceHandler; 54 | private BleScanner bleScanner; 55 | private BleAdvertiser bleAdvertiser; 56 | private BeaconCache beaconCache; 57 | 58 | private UUID currentUUID; 59 | 60 | private BroadcastRepository broadcastRepository; 61 | 62 | private final IBinder mBinder = new TracingServiceBinder(); 63 | 64 | public static final int STATUS_RUNNING = 0; 65 | public static final int STATUS_STARTING = 1; 66 | public static final int STATUS_BLUETOOTH_NOT_ENABLED = 2; 67 | public static final int STATUS_LOCATION_NOT_ENABLED = 3; 68 | 69 | private int serviceStatus = STATUS_STARTING; 70 | 71 | public class TracingServiceBinder extends Binder { 72 | public int getServiceStatus() { 73 | return serviceStatus; 74 | } 75 | 76 | public void addServiceStatusListener(ServiceStatusListener listener) { 77 | serviceStatusListeners.add(listener); 78 | } 79 | 80 | public void removeServiceStatusListener(ServiceStatusListener listener) { 81 | serviceStatusListeners.remove(listener); 82 | } 83 | 84 | public double[] getNearbyDevices() { 85 | return beaconCache.getNearbyDevices(); 86 | } 87 | 88 | public void addNearbyDevicesListener(BeaconCache.NearbyDevicesListener listener) { 89 | beaconCache.nearbyDevicesListeners.add(listener); 90 | } 91 | 92 | public void removeNearbyDevicesListener(BeaconCache.NearbyDevicesListener listener) { 93 | beaconCache.nearbyDevicesListeners.remove(listener); 94 | } 95 | } 96 | 97 | List serviceStatusListeners = new ArrayList<>(); 98 | public interface ServiceStatusListener { 99 | void serviceStatusChanged(int serviceStatus); 100 | } 101 | 102 | private void setServiceStatus(int serviceStatus) { 103 | this.serviceStatus = serviceStatus; 104 | for (ServiceStatusListener listener : serviceStatusListeners) { 105 | listener.serviceStatusChanged(serviceStatus); 106 | } 107 | } 108 | 109 | private Runnable regenerateUUID = () -> { 110 | Log.i(LOG_TAG, "Regenerating UUID"); 111 | 112 | currentUUID = UUID.randomUUID(); 113 | long time = System.currentTimeMillis(); 114 | broadcastRepository.insertOwnUUID(new OwnUUID(currentUUID, new Date(time))); 115 | 116 | // Convert the UUID to its SHA-256 hash 117 | ByteBuffer inputBuffer = ByteBuffer.wrap(new byte[/*Long.BYTES*/ 8 * 2]); 118 | inputBuffer.putLong(0, currentUUID.getMostSignificantBits()); 119 | inputBuffer.putLong(4, currentUUID.getLeastSignificantBits()); 120 | 121 | byte[] broadcastData; 122 | 123 | try { 124 | MessageDigest digest = MessageDigest.getInstance("SHA-256"); 125 | broadcastData = digest.digest(inputBuffer.array()); 126 | broadcastData = Arrays.copyOf(broadcastData, BROADCAST_LENGTH); 127 | broadcastData[HASH_LENGTH] = getTransmitPower(); 128 | } catch (NoSuchAlgorithmException e) { 129 | Log.wtf(LOG_TAG, "Algorithm not found", e); 130 | throw new RuntimeException(e); 131 | } 132 | 133 | bleAdvertiser.setBroadcastData(broadcastData); 134 | 135 | serviceHandler.removeCallbacks(this.regenerateUUID); 136 | serviceHandler.postDelayed(this.regenerateUUID, UUID_VALID_TIME); 137 | }; 138 | 139 | private byte getTransmitPower() { 140 | // TODO look up transmit power for current device 141 | return (byte) -65; 142 | } 143 | 144 | @Override 145 | public void onCreate() { 146 | super.onCreate(); 147 | broadcastRepository = new BroadcastRepository(this.getApplication()); 148 | HandlerThread thread = new HandlerThread("TrackerHandler", Thread.NORM_PRIORITY); 149 | thread.start(); 150 | 151 | // Get the HandlerThread's Looper and use it for our Handler 152 | serviceLooper = thread.getLooper(); 153 | serviceHandler = new Handler(serviceLooper); 154 | beaconCache = new BeaconCache(broadcastRepository, serviceHandler); 155 | 156 | IntentFilter filter = new IntentFilter(); 157 | filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 158 | filter.addAction(LocationManager.MODE_CHANGED_ACTION); 159 | registerReceiver(stateReceiver, filter); 160 | } 161 | 162 | @TargetApi(26) 163 | private void createChannel(NotificationManager notificationManager) { 164 | int importance = NotificationManager.IMPORTANCE_DEFAULT; 165 | 166 | NotificationChannel mChannel = new NotificationChannel(DEFAULT_NOTIFICATION_CHANNEL, DEFAULT_NOTIFICATION_CHANNEL, importance); 167 | mChannel.setDescription(getText(R.string.notification_channel).toString()); 168 | mChannel.enableLights(true); 169 | mChannel.setLightColor(Color.BLUE); 170 | mChannel.setImportance(NotificationManager.IMPORTANCE_LOW); 171 | notificationManager.createNotificationChannel(mChannel); 172 | } 173 | 174 | private void runAsForgroundService() { 175 | NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); 176 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) 177 | createChannel(notificationManager); 178 | 179 | Intent notificationIntent = new Intent(this, MainActivity.class); 180 | 181 | PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, 182 | notificationIntent, 0); 183 | 184 | Notification notification = new NotificationCompat.Builder(this, 185 | DEFAULT_NOTIFICATION_CHANNEL) 186 | .setContentTitle(getText(R.string.notification_title)) 187 | .setContentText(getText(R.string.notification_message)) 188 | .setSmallIcon(R.mipmap.ic_launcher) 189 | .setContentIntent(pendingIntent) 190 | .setPriority(NotificationManager.IMPORTANCE_LOW) 191 | .setVibrate(null) 192 | .build(); 193 | 194 | startForeground(NOTIFICATION_ID, notification); 195 | } 196 | 197 | void stopBluetooth() { 198 | if (bleAdvertiser != null) { 199 | bleAdvertiser.stopAdvertising(); 200 | bleAdvertiser = null; 201 | } 202 | if (bleScanner != null) { 203 | bleScanner.stopScanning(); 204 | bleScanner = null; 205 | } 206 | 207 | //Stop regenerating UUIDs while bluetooth is not running 208 | serviceHandler.removeCallbacks(this.regenerateUUID); 209 | } 210 | 211 | void tryStartingBluetooth() { 212 | Log.i(LOG_TAG, "Try starting bluetooth advertisement + scanning"); 213 | if (serviceStatus == STATUS_RUNNING) { 214 | Log.i(LOG_TAG, "Bluetooth is already running, restarting"); 215 | //If bluetooth is already running, stop it again and try to restart 216 | //This is done to check that bluetooth and location is enabled again and 217 | //set an error state otherwise 218 | 219 | stopBluetooth(); 220 | } 221 | 222 | BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); 223 | assert bluetoothManager != null; 224 | BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter(); 225 | if (!bluetoothAdapter.isEnabled()) { 226 | Log.i(LOG_TAG, "Bluetooth not enabled"); 227 | setServiceStatus(STATUS_BLUETOOTH_NOT_ENABLED); 228 | return; 229 | } 230 | 231 | LocationManager locationManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE); 232 | assert locationManager != null; 233 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 234 | //I have read this is not actually required on all devices, but I have not found a way 235 | //to check if it is required. 236 | //If location is not enabled the BLE scan fails silently (scan callback is never called) 237 | if (!locationManager.isLocationEnabled()) { 238 | Log.i(LOG_TAG, "Location not enabled (API>=P check)"); 239 | setServiceStatus(STATUS_LOCATION_NOT_ENABLED); 240 | return; 241 | } 242 | } else { 243 | //Not sure if this is the correct check, gps is not really required, but passive provider 244 | //does not seem to be enough 245 | if (!locationManager.getProviders(true).contains(LocationManager.GPS_PROVIDER)) { 246 | Log.i(LOG_TAG, "Location not enabled (API

{ 20 | 21 | private List beacons = Collections.emptyList(); 22 | 23 | public static class EnvironmentDevicesViewHolder extends RecyclerView.ViewHolder { 24 | public LinearLayout layout; 25 | public TextView textViewDate; 26 | public TextView textViewDistance; 27 | 28 | public EnvironmentDevicesViewHolder(LinearLayout v) { 29 | super(v); 30 | layout = v; 31 | textViewDate = v.findViewById(R.id.nearby_devices_list_text_view_date); 32 | textViewDistance = v.findViewById(R.id.nearby_devices_list_text_view_distance); 33 | } 34 | } 35 | 36 | @NonNull 37 | @Override 38 | public EnvironmentDevicesViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 39 | LinearLayout l = (LinearLayout) LayoutInflater.from(parent.getContext()) 40 | .inflate(R.layout.nearby_devices_view, parent, false); 41 | EnvironmentDevicesViewHolder vh = new EnvironmentDevicesViewHolder(l); 42 | return vh; 43 | } 44 | 45 | @Override 46 | public void onBindViewHolder(@NonNull EnvironmentDevicesViewHolder holder, int position) { 47 | Beacon data = beacons.get(position); 48 | 49 | holder.textViewDate.setText(SimpleDateFormat.getDateTimeInstance().format(data.timestamp)); 50 | holder.textViewDistance.setText(String.format("%.1f m", data.distance)); 51 | } 52 | 53 | @Override 54 | public int getItemCount() { 55 | return beacons.size(); 56 | } 57 | 58 | public void setBeacons(List beacons) { 59 | this.beacons = beacons; 60 | notifyDataSetChanged(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/ui/InfectedUUIDsAdapter.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.ui; 2 | 3 | import android.content.Context; 4 | import android.text.format.DateFormat; 5 | import android.view.LayoutInflater; 6 | import android.view.ViewGroup; 7 | import android.widget.LinearLayout; 8 | import android.widget.TextView; 9 | 10 | import app.bandemic.R; 11 | 12 | import app.bandemic.strict.database.Infection; 13 | 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | import androidx.annotation.NonNull; 18 | import androidx.recyclerview.widget.RecyclerView; 19 | 20 | public class InfectedUUIDsAdapter extends RecyclerView.Adapter { 21 | 22 | private List infectedUUIDs = Collections.emptyList(); 23 | 24 | public static class InfectedUUIDsViewHolder extends RecyclerView.ViewHolder { 25 | public LinearLayout layout; 26 | public TextView textViewDate; 27 | public TextView textViewDisease; 28 | public TextView textViewDistance; 29 | public TextView textViewDanger; 30 | 31 | public InfectedUUIDsViewHolder(LinearLayout v) { 32 | super(v); 33 | layout = v; 34 | textViewDate = v.findViewById(R.id.infected_list_text_view_date); 35 | textViewDisease = v.findViewById(R.id.infected_list_text_view_disease); 36 | textViewDistance = v.findViewById(R.id.infected_list_text_view_distance); 37 | textViewDanger = v.findViewById(R.id.infected_list_text_view_danger); 38 | } 39 | } 40 | 41 | @NonNull 42 | @Override 43 | public InfectedUUIDsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 44 | LinearLayout l = (LinearLayout) LayoutInflater.from(parent.getContext()) 45 | .inflate(R.layout.infected_encounter_view, parent, false); 46 | InfectedUUIDsViewHolder vh = new InfectedUUIDsViewHolder(l); 47 | return vh; 48 | } 49 | 50 | @Override 51 | public void onBindViewHolder(@NonNull InfectedUUIDsViewHolder holder, int position) { 52 | Infection infection = infectedUUIDs.get(position); 53 | Context c = holder.layout.getContext(); 54 | 55 | java.text.DateFormat df = DateFormat.getDateFormat(c); 56 | holder.textViewDate.setText(df.format(infection.encounterDate)); 57 | holder.textViewDisease.setText(infection.icdCode); 58 | holder.textViewDistance.setText(String.format("%.1f m", infection.distance)); 59 | holder.textViewDanger.setText(infection.distrustLevel == 0 ? 60 | c.getResources().getString(R.string.verified) : 61 | c.getResources().getString(R.string.unverified)); 62 | } 63 | 64 | @Override 65 | public int getItemCount() { 66 | return infectedUUIDs.size(); 67 | } 68 | 69 | public void setInfectedUUIDs(List uuids) { 70 | this.infectedUUIDs = uuids; 71 | notifyDataSetChanged(); 72 | } 73 | 74 | public Infection getLastInfectedUUUID() { 75 | if(!infectedUUIDs.isEmpty()) { 76 | return infectedUUIDs.get(infectedUUIDs.size()-1); 77 | } 78 | return null; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/ui/Instructions.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.ui; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.TextView; 7 | 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import app.bandemic.R; 10 | 11 | public class Instructions extends AppCompatActivity { 12 | protected void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | setContentView(R.layout.instructions); 15 | } 16 | 17 | public void onNextClick(View v) { 18 | startActivity(new Intent(this, DataProtectionInfo.class)); 19 | finish(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.ui; 2 | 3 | import android.Manifest; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.ServiceConnection; 8 | import android.content.SharedPreferences; 9 | import android.content.pm.PackageManager; 10 | import android.os.Bundle; 11 | import android.os.IBinder; 12 | import android.util.Log; 13 | import android.view.View; 14 | 15 | import androidx.appcompat.app.AppCompatActivity; 16 | import androidx.core.app.ActivityCompat; 17 | import androidx.core.content.ContextCompat; 18 | import androidx.lifecycle.ViewModelProvider; 19 | import androidx.preference.PreferenceManager; 20 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 21 | 22 | import app.bandemic.R; 23 | import app.bandemic.fragments.ErrorMessageFragment; 24 | import app.bandemic.fragments.NearbyDevicesFragment; 25 | import app.bandemic.strict.service.BeaconCache; 26 | import app.bandemic.strict.service.TracingService; 27 | import app.bandemic.viewmodel.MainActivityViewModel; 28 | 29 | public class MainActivity extends AppCompatActivity { 30 | 31 | private static final String TAG = "MainActivity"; 32 | private final int MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 1; 33 | public static final String PREFERENCE_DATA_OK = "data_ok"; 34 | 35 | private MainActivityViewModel mViewModel; 36 | private NearbyDevicesFragment nearbyDevicesFragment; 37 | private TracingService.TracingServiceBinder serviceBinder; 38 | 39 | @Override 40 | protected void onCreate(Bundle savedInstanceState) { 41 | super.onCreate(savedInstanceState); 42 | setContentView(R.layout.activity_main); 43 | mViewModel = new ViewModelProvider(this).get(MainActivityViewModel.class); 44 | 45 | SwipeRefreshLayout refreshLayout = findViewById(R.id.main_swipe_refresh_layout); 46 | refreshLayout.setOnRefreshListener(() -> { 47 | mViewModel.onRefresh(); 48 | }); 49 | mViewModel.eventRefresh().observe(this, refreshing -> { 50 | refreshLayout.setRefreshing(refreshing); 51 | }); 52 | 53 | checkPermissions(); 54 | 55 | SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); 56 | if(!sharedPref.getBoolean(PREFERENCE_DATA_OK, false)) { 57 | startActivity(new Intent(this, Instructions.class)); 58 | } 59 | } 60 | 61 | private void checkPermissions() { 62 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) 63 | != PackageManager.PERMISSION_GRANTED) { 64 | // Permission is not granted 65 | ActivityCompat.requestPermissions(this, 66 | new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 67 | MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION); 68 | } 69 | else { 70 | startTracingService(); 71 | } 72 | } 73 | 74 | private void startTracingService() { 75 | Intent intent = new Intent(this, TracingService.class); 76 | startService(intent); 77 | bindService(intent, connection, Context.BIND_AUTO_CREATE); 78 | } 79 | 80 | @Override 81 | public void onRequestPermissionsResult(int requestCode, 82 | String[] permissions, int[] grantResults) { 83 | switch (requestCode) { 84 | case MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION: { 85 | // If request is cancelled, the result arrays are empty. 86 | if (grantResults.length > 0 87 | && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 88 | findViewById(R.id.card_permission_required).setVisibility(View.GONE); 89 | // Start background service 90 | startTracingService(); 91 | } else { 92 | // Don't start the discovery service 93 | findViewById(R.id.card_permission_required).setVisibility(View.VISIBLE); 94 | findViewById(R.id.ask_permission).setOnClickListener(view -> checkPermissions()); 95 | } 96 | return; 97 | } 98 | } 99 | } 100 | 101 | private TracingService.ServiceStatusListener serviceStatusListener = serviceStatus -> { 102 | Log.i(TAG, "Service status: " + serviceStatus); 103 | runOnUiThread(() -> { 104 | if (serviceStatus == TracingService.STATUS_RUNNING) { 105 | if (nearbyDevicesFragment == null) { 106 | nearbyDevicesFragment = new NearbyDevicesFragment(); 107 | } 108 | if (nearbyDevicesFragment.model != null) { 109 | nearbyDevicesFragment.model.distances.setValue(serviceBinder.getNearbyDevices()); 110 | } 111 | nearbyDevicesFragment.skipAnimations(); 112 | 113 | getSupportFragmentManager().beginTransaction() 114 | .setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out) 115 | .replace(R.id.fragment_nearby_devices, nearbyDevicesFragment) 116 | .commit(); 117 | } else { 118 | String errorMessage = ""; 119 | if (serviceStatus == TracingService.STATUS_BLUETOOTH_NOT_ENABLED) { 120 | errorMessage = getString(R.string.error_bluetooth_not_enabled); 121 | } else if (serviceStatus == TracingService.STATUS_LOCATION_NOT_ENABLED) { 122 | errorMessage = getString(R.string.error_location_not_enabled); 123 | } 124 | ErrorMessageFragment errorMessageFragment = ErrorMessageFragment.newInstance(errorMessage); 125 | getSupportFragmentManager().beginTransaction() 126 | .setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out) 127 | .replace(R.id.fragment_nearby_devices, errorMessageFragment) 128 | .commit(); 129 | } 130 | }); 131 | }; 132 | 133 | BeaconCache.NearbyDevicesListener nearbyDevicesListener = new BeaconCache.NearbyDevicesListener() { 134 | @Override 135 | public void onNearbyDevicesChanged(double[] distances) { 136 | runOnUiThread(() -> { 137 | if (nearbyDevicesFragment != null) { 138 | nearbyDevicesFragment.model.distances.setValue(distances); 139 | } 140 | }); 141 | } 142 | }; 143 | 144 | @Override 145 | protected void onStop() { 146 | serviceBinder.removeNearbyDevicesListener(nearbyDevicesListener); 147 | serviceBinder.removeServiceStatusListener(serviceStatusListener); 148 | unbindService(connection); 149 | super.onStop(); 150 | } 151 | 152 | private ServiceConnection connection = new ServiceConnection() { 153 | 154 | @Override 155 | public void onServiceConnected(ComponentName className, IBinder service) { 156 | Log.i(TAG, "Service connected"); 157 | serviceBinder = (TracingService.TracingServiceBinder) service; 158 | 159 | serviceBinder.addServiceStatusListener(serviceStatusListener); 160 | serviceStatusListener.serviceStatusChanged(serviceBinder.getServiceStatus()); 161 | 162 | serviceBinder.addNearbyDevicesListener(nearbyDevicesListener); 163 | } 164 | 165 | @Override 166 | public void onServiceDisconnected(ComponentName arg0) { 167 | Log.i(TAG, "Service disconnected"); 168 | serviceBinder = null; 169 | } 170 | }; 171 | 172 | } 173 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/viewmodel/EnvironmentLoggerViewModel.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.viewmodel; 2 | 3 | import android.app.Application; 4 | 5 | import app.bandemic.strict.database.Beacon; 6 | import app.bandemic.strict.repository.BroadcastRepository; 7 | 8 | import java.util.List; 9 | 10 | import androidx.lifecycle.AndroidViewModel; 11 | import androidx.lifecycle.LiveData; 12 | 13 | public class EnvironmentLoggerViewModel extends AndroidViewModel { 14 | 15 | private BroadcastRepository mBroadcastRepository; 16 | 17 | private LiveData> mDistinctBeacons; 18 | 19 | public EnvironmentLoggerViewModel(Application application) { 20 | super(application); 21 | //TODO: are two instances of repository ok (in ViewModel and TracingService)? 22 | mBroadcastRepository = new BroadcastRepository(application); 23 | mDistinctBeacons = mBroadcastRepository.getDistinctBeacons(); 24 | } 25 | 26 | //todo do I need a refresh function as for uuids? this should update automatically 27 | 28 | public LiveData> getDistinctBeacons() { 29 | return mDistinctBeacons; 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/viewmodel/InfectionCheckViewModel.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.viewmodel; 2 | 3 | import android.app.Application; 4 | 5 | import app.bandemic.strict.database.Infection; 6 | import app.bandemic.strict.repository.InfectedUUIDRepository; 7 | 8 | import java.util.List; 9 | 10 | import androidx.lifecycle.AndroidViewModel; 11 | import androidx.lifecycle.LiveData; 12 | 13 | public class InfectionCheckViewModel extends AndroidViewModel { 14 | 15 | private InfectedUUIDRepository mRepository; 16 | 17 | private LiveData> possiblyInfectedEncounters; 18 | 19 | public InfectionCheckViewModel(Application application) { 20 | super(application); 21 | mRepository = new InfectedUUIDRepository(application); 22 | possiblyInfectedEncounters = mRepository.getPossiblyInfectedEncounters(); 23 | } 24 | 25 | public void refreshInfectedUUIDs() { 26 | mRepository.refreshInfectedUUIDs(); 27 | } 28 | 29 | public LiveData> getPossiblyInfectedEncounters() { return possiblyInfectedEncounters; } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/viewmodel/MainActivityViewModel.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.viewmodel; 2 | 3 | import android.app.Application; 4 | 5 | import androidx.lifecycle.AndroidViewModel; 6 | import androidx.lifecycle.LiveData; 7 | import androidx.lifecycle.MutableLiveData; 8 | 9 | public class MainActivityViewModel extends AndroidViewModel { 10 | 11 | private MutableLiveData eventRefresh; 12 | 13 | public LiveData eventRefresh() { 14 | if(eventRefresh == null) { 15 | eventRefresh = new MutableLiveData<>(); 16 | } 17 | return eventRefresh; 18 | } 19 | 20 | public void onRefresh() { 21 | eventRefresh.setValue(true); 22 | } 23 | 24 | public void finishRefresh() { 25 | eventRefresh.setValue(false); 26 | } 27 | 28 | public MainActivityViewModel(Application application) { 29 | super(application); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/app/bandemic/viewmodel/NearbyDevicesViewModel.java: -------------------------------------------------------------------------------- 1 | package app.bandemic.viewmodel; 2 | 3 | import androidx.lifecycle.MutableLiveData; 4 | import androidx.lifecycle.ViewModel; 5 | 6 | public class NearbyDevicesViewModel extends ViewModel { 7 | public MutableLiveData distances = new MutableLiveData<>(new double[0]); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/wvvszene2und3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ito-org/android-app/0fdadbe29c1f4960409d05213720eeda96de84b9/app/src/main/res/drawable-hdpi/wvvszene2und3.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/wvvszene4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ito-org/android-app/0fdadbe29c1f4960409d05213720eeda96de84b9/app/src/main/res/drawable-hdpi/wvvszene4.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/wwwszene1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ito-org/android-app/0fdadbe29c1f4960409d05213720eeda96de84b9/app/src/main/res/drawable-hdpi/wwwszene1.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_icon_bg.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_icon_fg.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_status_error.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_status_ok.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_status_time.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/nearby_device.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 19 | 20 | 30 | 31 | 36 | 37 | 43 | 44 | 51 | 52 | 53 | 54 | 55 | 60 | 61 | 65 | 66 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/res/layout/data_protection_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 34 | 35 |