Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a mechanism which allows app developers to declare lock screen policy (copy of #1861) #2445

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ object Dependencies {
const val core = "androidx.test:core:${Versions.AndroidxTest.core}"
const val extJunit = "androidx.test.ext:junit:${Versions.AndroidxTest.extJunit}"
const val extJunitKtx = "androidx.test.ext:junit-ktx:${Versions.AndroidxTest.extJunit}"
const val extTruth = "androidx.test.ext:truth:${Versions.AndroidxTest.extTruth}"
const val fragmentTesting =
"androidx.fragment:fragment-testing:${Versions.AndroidxTest.fragmentVersion}"
const val rules = "androidx.test:rules:${Versions.AndroidxTest.rules}"
Expand Down Expand Up @@ -291,21 +292,22 @@ object Dependencies {

// Test dependencies
object AndroidxTest {
const val archCore = "2.2.0"
const val benchmarkJUnit = "1.1.1"
const val core = "1.5.0"
const val archCore = "2.2.0"
const val extJunit = "1.1.5"
const val extTruth = "1.5.0"
const val fragmentVersion = "1.6.0"
const val rules = "1.5.0"
const val runner = "1.5.0"
const val fragmentVersion = "1.6.0"
}

const val espresso = "3.5.1"
const val jacoco = "0.8.10"
const val junit = "4.13.2"
const val mockitoKotlin = "3.2.0"
const val mockitoInline = "4.0.0"
const val robolectric = "4.10.3"
const val robolectric = "4.11.1"

object Mlkit {
const val barcodeScanning = "16.1.1"
Expand Down
20 changes: 18 additions & 2 deletions contrib/locationwidget/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<application>
<receiver
android:name="com.google.android.fhir.security.SecurityCheckReceiver"
android:exported="false"
>
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action
android:name="android.intent.action.MY_PACKAGE_REPLACED"
/>
</intent-filter>
</receiver>
</application>

<uses-permission
android:name="android.permission.REQUEST_PASSWORD_COMPLEXITY"
/>
</manifest>
6 changes: 6 additions & 0 deletions demo/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<receiver
android:name="com.google.android.fhir.demo.security.SecurityRequirementViolationReceiver"
android:exported="false"
/>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Google LLC
* Copyright 2023-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,7 +17,9 @@
package com.google.android.fhir.demo

import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import com.google.android.fhir.DatabaseErrorStrategy.RECREATE_AT_OPEN
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.FhirEngineConfiguration
Expand All @@ -26,8 +28,14 @@ import com.google.android.fhir.NetworkConfiguration
import com.google.android.fhir.ServerConfiguration
import com.google.android.fhir.datacapture.DataCaptureConfig
import com.google.android.fhir.datacapture.XFhirQueryResolver
import com.google.android.fhir.demo.security.SecurityRequirementViolationReceiver
import com.google.android.fhir.search.search
import com.google.android.fhir.security.FhirSecurityConfiguration
import com.google.android.fhir.security.LockScreenComplexity
import com.google.android.fhir.security.LockScreenRequirement
import com.google.android.fhir.security.RequirementViolationAction
import com.google.android.fhir.sync.remote.HttpLogger
import java.util.EnumSet
import timber.log.Timber

class FhirApplication : Application(), DataCaptureConfig.Provider {
Expand All @@ -46,19 +54,33 @@ class FhirApplication : Application(), DataCaptureConfig.Provider {
FhirEngineProvider.init(
FhirEngineConfiguration(
enableEncryptionIfSupported = true,
RECREATE_AT_OPEN,
ServerConfiguration(
"https://hapi.fhir.org/baseR4/",
httpLogger =
HttpLogger(
HttpLogger.Configuration(
if (BuildConfig.DEBUG) HttpLogger.Level.BODY else HttpLogger.Level.BASIC,
),
) {
Timber.tag("App-HttpLog").d(it)
},
networkConfiguration = NetworkConfiguration(uploadWithGzip = false),
),
databaseErrorStrategy = RECREATE_AT_OPEN,
securityConfiguration =
FhirSecurityConfiguration(
LockScreenRequirement(
complexity = LockScreenComplexity.HIGH,
EnumSet.noneOf(RequirementViolationAction::class.java),
),
PendingIntent.getBroadcast(
applicationContext,
/* requestCode= */ 0,
Intent(applicationContext, SecurityRequirementViolationReceiver::class.java),
/* flags= */ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
),
),
serverConfiguration =
ServerConfiguration(
"https://hapi.fhir.org/baseR4/",
httpLogger =
HttpLogger(
HttpLogger.Configuration(
if (BuildConfig.DEBUG) HttpLogger.Level.BODY else HttpLogger.Level.BASIC,
),
) {
Timber.tag("App-HttpLog").d(it)
},
networkConfiguration = NetworkConfiguration(uploadWithGzip = false),
),
),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright 2022-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.demo.security

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.admin.DevicePolicyManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi
import com.google.android.fhir.demo.R
import com.google.android.fhir.security.LockScreenRequirementViolation
import com.google.android.fhir.security.SecurityRequirementViolation.EXTRA_LOCK_SCREEN_REQUIREMENT_VIOLATION
import timber.log.Timber

/** A sample receiver demonstrates the handling of password requirement violation. */
class SecurityRequirementViolationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val lockScreenRequirementViolation =
intent.getParcelableExtra<LockScreenRequirementViolation>(
EXTRA_LOCK_SCREEN_REQUIREMENT_VIOLATION,
)
Timber.w("Lock screen violation extra: $lockScreenRequirementViolation")
if (lockScreenRequirementViolation == null) return
showPasswordViolationNotification(context, lockScreenRequirementViolation.requiredComplexity)
}

private fun showPasswordViolationNotification(context: Context, requiredComplexity: Int) {
val notificationManager = context.getSystemService(NotificationManager::class.java)
// We can't ask notification runtime permission from the background. Let's ask users for the
// notification running permission when the demo app activity is started.
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
!notificationManager.areNotificationsEnabled()
) {
Timber.w("Can't post notification")
return
}

createNotificationChannel(notificationManager)

val notificationBuilder =
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Notification.Builder(context)
} else {
Notification.Builder(context, SECURITY_REQUIREMENT_VIOLATION_NOTIFICATION_CHANNEL_ID)
}

notificationManager.notify(
NOTIFICATION_ID,
notificationBuilder
.setSmallIcon(R.drawable.ic_small_fire_engine)
.setContentTitle("Your lock screen doesn't meet the requirement")
.setContentText("Click to set up.")
.setContentIntent(
PendingIntent.getActivity(
context,
/* requestCode= */ 0,
Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD)
.putExtra(DevicePolicyManager.EXTRA_PASSWORD_COMPLEXITY, requiredComplexity),
/* flags= */ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
),
)
.setAutoCancel(true)
.build(),
)
}

private fun createNotificationChannel(notificationManager: NotificationManager) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return

notificationManager.createNotificationChannel(securityNotificationChannel)
}

private companion object {
const val NOTIFICATION_ID = 324265
const val SECURITY_REQUIREMENT_VIOLATION_NOTIFICATION_CHANNEL_ID = "fhir_security"

@RequiresApi(Build.VERSION_CODES.O)
val securityNotificationChannel =
NotificationChannel(
SECURITY_REQUIREMENT_VIOLATION_NOTIFICATION_CHANNEL_ID,
/* name= */ "Security requirement violation notification",
NotificationManager.IMPORTANCE_DEFAULT,
)
}
}
13 changes: 13 additions & 0 deletions demo/src/main/res/drawable/ic_small_fire_engine.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="48"
android:viewportWidth="48"
>
<path
android:fillColor="@android:color/white"
android:pathData="M34,42C35.667,42 37.083,41.417 38.25,40.25C39.417,39.083 40,37.667 40,36H43C43.833,36 44.542,35.708 45.125,35.125C45.708,34.542 46,33.833 46,33V22H24V13C24,12.167 23.708,11.458 23.125,10.875C22.542,10.292 21.833,10 21,10H15.5V7.5C15.5,7.067 15.358,6.708 15.075,6.425C14.792,6.142 14.433,6 14,6H12C11.567,6 11.208,6.142 10.925,6.425C10.642,6.708 10.5,7.067 10.5,7.5V10H8.15C7.483,10 6.892,10.183 6.375,10.55C5.858,10.917 5.5,11.417 5.3,12.05L2.15,21.5C2.083,21.667 2.042,21.825 2.025,21.975C2.008,22.125 2,22.283 2,22.45V36H8C8,37.667 8.583,39.083 9.75,40.25C10.917,41.417 12.333,42 14,42C15.667,42 17.083,41.417 18.25,40.25C19.417,39.083 20,37.667 20,36H28C28,37.667 28.583,39.083 29.75,40.25C30.917,41.417 32.333,42 34,42ZM34,39C33.133,39 32.417,38.708 31.85,38.125C31.283,37.542 31,36.833 31,36C31,35.133 31.283,34.417 31.85,33.85C32.417,33.283 33.133,33 34,33C34.833,33 35.542,33.283 36.125,33.85C36.708,34.417 37,35.133 37,36C37,36.833 36.708,37.542 36.125,38.125C35.542,38.708 34.833,39 34,39ZM14,39C13.133,39 12.417,38.708 11.85,38.125C11.283,37.542 11,36.833 11,36C11,35.133 11.283,34.417 11.85,33.85C12.417,33.283 13.133,33 14,33C14.833,33 15.542,33.283 16.125,33.85C16.708,34.417 17,35.133 17,36C17,36.833 16.708,37.542 16.125,38.125C15.542,38.708 14.833,39 14,39ZM43,25V33H39.2C38.667,32.1 37.942,31.375 37.025,30.825C36.108,30.275 35.1,30 34,30C32.9,30 31.892,30.275 30.975,30.825C30.058,31.375 29.333,32.1 28.8,33H24V25H43ZM21,33H19.2C18.667,32.1 17.942,31.375 17.025,30.825C16.108,30.275 15.1,30 14,30C12.9,30 11.892,30.275 10.975,30.825C10.058,31.375 9.333,32.1 8.8,33H5V25H21V33ZM21,22H5.15L8.15,13H21V22ZM46,20V18H44V12H46V10H26V12H28V18H26V20H46ZM42,18H37V12H42V18ZM35,18H30V12H35V18Z"
/>
</vector>
1 change: 1 addition & 0 deletions engine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ dependencies {

testImplementation(Dependencies.AndroidxTest.archCore)
testImplementation(Dependencies.AndroidxTest.core)
testImplementation(Dependencies.AndroidxTest.extTruth)
testImplementation(Dependencies.AndroidxTest.workTestingRuntimeKtx)
testImplementation(Dependencies.Kotlin.kotlinCoroutinesTest)
testImplementation(Dependencies.junit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package com.google.android.fhir

import android.content.Context
import com.google.android.fhir.DatabaseErrorStrategy.UNSPECIFIED
import com.google.android.fhir.security.FhirSecurityConfiguration
import com.google.android.fhir.security.SecurityRequirementsManager
import com.google.android.fhir.sync.DataSource
import com.google.android.fhir.sync.FhirDataStore
import com.google.android.fhir.sync.HttpAuthenticator
Expand Down Expand Up @@ -67,6 +69,11 @@ object FhirEngineProvider {
return getOrCreateFhirService(context).fhirDataStore
}

@Synchronized
fun getSecurityRequirementsManager(context: Context): SecurityRequirementsManager {
return getOrCreateFhirService(context).securityRequirementsManager
}

@Synchronized
private fun getOrCreateFhirService(context: Context): FhirServices {
if (fhirServices == null) {
Expand All @@ -82,6 +89,9 @@ object FhirEngineProvider {
if (configuration.testMode) {
inMemory()
}
if (configuration.securityConfiguration != null) {
setSecurityConfiguration(configuration.securityConfiguration)
}
}
.build()
}
Expand Down Expand Up @@ -115,6 +125,7 @@ object FhirEngineProvider {
data class FhirEngineConfiguration(
val enableEncryptionIfSupported: Boolean = false,
val databaseErrorStrategy: DatabaseErrorStrategy = UNSPECIFIED,
val securityConfiguration: FhirSecurityConfiguration? = null,
val serverConfiguration: ServerConfiguration? = null,
val testMode: Boolean = false,
/**
Expand Down
11 changes: 11 additions & 0 deletions engine/src/main/java/com/google/android/fhir/FhirServices.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import com.google.android.fhir.db.impl.DatabaseImpl
import com.google.android.fhir.impl.FhirEngineImpl
import com.google.android.fhir.index.ResourceIndexer
import com.google.android.fhir.index.SearchParamDefinitionsProviderImpl
import com.google.android.fhir.security.FhirSecurityConfiguration
import com.google.android.fhir.security.SecurityRequirementsManager
import com.google.android.fhir.sync.DataSource
import com.google.android.fhir.sync.FhirDataStore
import com.google.android.fhir.sync.remote.FhirHttpDataSource
Expand All @@ -41,13 +43,15 @@ internal data class FhirServices(
val database: Database,
val remoteDataSource: DataSource? = null,
val fhirDataStore: FhirDataStore,
val securityRequirementsManager: SecurityRequirementsManager,
) {
class Builder(private val context: Context) {
private var inMemory: Boolean = false
private var enableEncryption: Boolean = false
private var databaseErrorStrategy = DatabaseErrorStrategy.UNSPECIFIED
private var serverConfiguration: ServerConfiguration? = null
private var searchParameters: List<SearchParameter>? = null
private var securityConfiguration: FhirSecurityConfiguration? = null

internal fun inMemory() = apply { inMemory = true }

Expand All @@ -71,6 +75,11 @@ internal data class FhirServices(
this.searchParameters = searchParameters
}

internal fun setSecurityConfiguration(securityConfiguration: FhirSecurityConfiguration) =
apply {
this.securityConfiguration = securityConfiguration
}

fun build(): FhirServices {
val parser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser()
val terser = FhirTerser(FhirContext.forCached(FhirVersionEnum.R4))
Expand All @@ -95,12 +104,14 @@ internal data class FhirServices(
.build(),
)
}
val securityRequirementsManager = SecurityRequirementsManager(context, securityConfiguration)
return FhirServices(
fhirEngine = engine,
parser = parser,
database = db,
remoteDataSource = remoteDataSource,
fhirDataStore = FhirDataStore(context),
securityRequirementsManager = securityRequirementsManager,
)
}
}
Expand Down
Loading
Loading