├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── ldlywt │ │ └── commoncode │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── ldlywt │ │ │ └── commoncode │ │ │ ├── App.kt │ │ │ ├── MainActivity.kt │ │ │ ├── activity │ │ │ └── LiveDataTestActivity.kt │ │ │ ├── livedata │ │ │ ├── NetworkWatchLiveData.kt │ │ │ ├── RequestPermissionLiveData.kt │ │ │ ├── TakePhotoLiveData.kt │ │ │ └── TimerGlobalLiveData.kt │ │ │ ├── location │ │ │ ├── FusedLocationHelper.kt │ │ │ ├── LocationHelper.kt │ │ │ ├── LocationHelperV2.kt │ │ │ ├── LocationPermissionUtils.kt │ │ │ └── NetWorkLocationHelper.kt │ │ │ ├── permission │ │ │ └── PermissionKtx.kt │ │ │ └── view │ │ │ └── LifecycleView.kt │ └── res │ │ ├── color │ │ ├── color_md_contained_btn_background.xml │ │ ├── color_md_contained_btn_stroke.xml │ │ ├── color_md_contained_btn_text.xml │ │ ├── color_md_dashed_btn_text.xml │ │ ├── color_md_outlined_btn_background.xml │ │ ├── color_md_outlined_btn_stroke.xml │ │ ├── color_md_outlined_btn_text.xml │ │ ├── color_md_text_btn_background.xml │ │ └── color_md_text_btn_text.xml │ │ ├── drawable │ │ └── bg_button_dash_selector.xml │ │ ├── layout │ │ ├── activity_live_data_test.xml │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── ldlywt │ └── commoncode │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ktx ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── ldlywt │ │ └── ktx │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── ldlywt │ │ └── ktx │ │ ├── ActivityKtx.kt │ │ ├── Cursor.kt │ │ ├── EditText.kt │ │ ├── File.kt │ │ ├── FlowKtx.kt │ │ ├── Network.kt │ │ ├── StringKtx.kt │ │ ├── Version.kt │ │ ├── ViewKtx.kt │ │ ├── clipboard.kt │ │ ├── dp.kt │ │ ├── hidekeyboard.kt │ │ ├── snackbar.kt │ │ └── toast.kt │ └── test │ └── java │ └── com │ └── ldlywt │ └── ktx │ └── ExampleUnitTest.kt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Common Generic Code Organizer 2 | 3 | ## Outline 4 | 5 | [![o5MSdP.md.png](https://s1.ax1x.com/2021/12/10/o5MSdP.md.png)](https://imgtu.com/i/o5MSdP) 6 | 7 | ## MaterialDesign Button Styles 8 | 9 | [![o5Qe6H.md.png](https://s1.ax1x.com/2021/12/10/o5Qe6H.md.png)](https://imgtu.com/i/o5Qe6H) 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | applicationId "com.ldlywt.commoncode" 11 | minSdk 24 12 | targetSdk 31 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | viewBinding { 33 | enabled = true 34 | } 35 | } 36 | 37 | dependencies { 38 | 39 | implementation 'androidx.core:core-ktx:1.6.0' 40 | implementation 'androidx.appcompat:appcompat:1.3.1' 41 | implementation 'com.google.android.material:material:1.4.0' 42 | implementation 'androidx.constraintlayout:constraintlayout:2.1.1' 43 | implementation project(path: ':ktx') 44 | testImplementation 'junit:junit:4.+' 45 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 46 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 47 | implementation 'com.google.android.gms:play-services-location:18.0.0' 48 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' 49 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1' 50 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-rc01' 51 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ldlywt/commoncode/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ldlywt.commoncode", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/App.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode 2 | 3 | import android.app.Application 4 | 5 | val applicationContext = App.instance 6 | 7 | class App : Application() { 8 | 9 | override fun onCreate() { 10 | super.onCreate() 11 | instance = this 12 | } 13 | 14 | companion object { 15 | lateinit var instance: App 16 | private set 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import android.os.Bundle 7 | import androidx.activity.result.ActivityResult 8 | import androidx.activity.result.ActivityResultLauncher 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import com.ldlywt.commoncode.activity.LiveDataTestActivity 11 | import com.ldlywt.commoncode.databinding.ActivityMainBinding 12 | import com.ldlywt.commoncode.ktx.toast 13 | import com.ldlywt.commoncode.view.LifecycleView 14 | 15 | class MainActivity : AppCompatActivity(R.layout.activity_main) { 16 | 17 | private val mBinding by lazy { ActivityMainBinding.inflate(layoutInflater) } 18 | 19 | private val activityResultLauncher: ActivityResultLauncher = 20 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult: ActivityResult -> 21 | if (activityResult.resultCode == Activity.RESULT_OK) { 22 | toast(activityResult.data?.getStringExtra("key") ?: "") 23 | } 24 | } 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | setContentView(mBinding.root) 29 | mBinding.btLiveData.setOnClickListener { 30 | activityResultLauncher.launch(Intent(this, LiveDataTestActivity::class.java)) 31 | } 32 | mBinding.root.addView(LifecycleView(this, lifecycleOwner = this)) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/activity/LiveDataTestActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode.activity 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.util.Log 8 | import androidx.activity.result.ActivityResultLauncher 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.appcompat.app.AppCompatActivity 11 | import androidx.lifecycle.Lifecycle 12 | import androidx.lifecycle.lifecycleScope 13 | import com.ldlywt.commoncode.R 14 | import com.ldlywt.commoncode.databinding.ActivityLiveDataTestBinding 15 | import com.ldlywt.ktx.launchAndCollectIn 16 | import com.ldlywt.commoncode.ktx.toast 17 | import com.ldlywt.commoncode.livedata.RequestPermissionLiveData 18 | import com.ldlywt.commoncode.livedata.TakePhotoLiveData 19 | import com.ldlywt.commoncode.livedata.TimerGlobalLiveData 20 | import com.ldlywt.commoncode.location.LocationHelperV2 21 | import com.ldlywt.commoncode.location.LocationPermissionUtils 22 | import com.ldlywt.commoncode.location.NetWorkLocationHelper 23 | import kotlinx.coroutines.launch 24 | 25 | class LiveDataTestActivity : AppCompatActivity(R.layout.activity_live_data_test) { 26 | 27 | private val mBinding by lazy { ActivityLiveDataTestBinding.inflate(layoutInflater) } 28 | 29 | private var takePhotoLiveData: TakePhotoLiveData = 30 | TakePhotoLiveData(activityResultRegistry, "key") 31 | 32 | private var requestPermissionLiveData = RequestPermissionLiveData(activityResultRegistry, "key") 33 | 34 | private val requestLocationPermissionLauncher: ActivityResultLauncher = 35 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { result: Boolean -> 36 | toast("request permission $result") 37 | if (result) { 38 | lifecycleScope.launch { 39 | // val location = NetWorkLocationHelper().getNetLocation(this@LiveDataTestActivity) 40 | // Log.i("wutao--> ", "location:: $location") 41 | // NetWorkLocationHelper(this@LiveDataTestActivity, lifecycleScope) 42 | // .getNetLocationFlow() 43 | // .buffer(Channel.CONFLATED) 44 | // .debounce(300) 45 | // .collect { location -> 46 | // Log.i("wutao--> ", "location:: $location") 47 | // } 48 | 49 | val location = LocationHelperV2(this@LiveDataTestActivity, lifecycleScope).getLocation() 50 | Log.i("wutao--> ", "val location = : $location") 51 | } 52 | } 53 | } 54 | 55 | override fun onCreate(savedInstanceState: Bundle?) { 56 | super.onCreate(savedInstanceState) 57 | setContentView(mBinding.root) 58 | init() 59 | requestLocationWhenOnStart() 60 | } 61 | 62 | private fun requestLocationWhenOnStart() { 63 | if (LocationPermissionUtils.isLocationPermissionGranted(this)) { 64 | NetWorkLocationHelper(this, lifecycleScope) 65 | .getNetLocationFlow() 66 | .launchAndCollectIn(this, Lifecycle.State.RESUMED) { 67 | Log.i("wutao--> ", "New Location : $it") 68 | } 69 | } 70 | } 71 | 72 | private fun init() { 73 | 74 | takePhotoLiveData.observe(this) { bitmap -> mBinding.imageView.setImageBitmap(bitmap) } 75 | 76 | mBinding.btTakePhoto.setOnClickListener { takePhotoLiveData.takePhoto() } 77 | 78 | mBinding.btStopTimer.setOnClickListener { 79 | //启动全局计算器 80 | //TimerGlobalLiveData.get().startTimer() 81 | TimerGlobalLiveData.get().cancelTimer() 82 | } 83 | 84 | TimerGlobalLiveData.get().observe(this) { Log.i("LiveDataTestActivity", "GlobalTimer value: == $it") } 85 | 86 | mBinding.btRequestPermission.setOnClickListener { requestLocationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) } 87 | 88 | mBinding.btRequestPermissionV2.setOnClickListener { requestPermissionLiveData.requestPermission(Manifest.permission.RECORD_AUDIO) } 89 | 90 | requestPermissionLiveData.observe(this) { toast("权限RECORD_AUDIO请求结果 $it") } 91 | 92 | mBinding.btBack.setOnClickListener { 93 | setResult(Activity.RESULT_OK, Intent().putExtra("key", "返回消息")) 94 | finish() 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/livedata/NetworkWatchLiveData.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode.livedata 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.IntentFilter 8 | import android.net.ConnectivityManager 9 | import android.net.NetworkInfo 10 | import androidx.annotation.MainThread 11 | import androidx.lifecycle.LiveData 12 | 13 | class NetworkWatchLiveData(context: Context) : LiveData() { 14 | private val mContext = context.applicationContext 15 | private val mNetworkReceiver: NetworkReceiver = NetworkReceiver() 16 | private val mIntentFilter: IntentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) 17 | 18 | override fun onActive() { 19 | mContext.registerReceiver(mNetworkReceiver, mIntentFilter) 20 | } 21 | 22 | override fun onInactive() = mContext.unregisterReceiver(mNetworkReceiver) 23 | 24 | private class NetworkReceiver : BroadcastReceiver() { 25 | @SuppressLint("MissingPermission") 26 | override fun onReceive(context: Context, intent: Intent) { 27 | val manager = 28 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 29 | val activeNetwork = manager.activeNetworkInfo 30 | sInstance.postValue(activeNetwork) 31 | } 32 | } 33 | 34 | companion object { 35 | 36 | private lateinit var sInstance: NetworkWatchLiveData 37 | 38 | @MainThread 39 | fun get(context: Context): NetworkWatchLiveData { 40 | sInstance = if (Companion::sInstance.isInitialized) sInstance else NetworkWatchLiveData(context) 41 | return sInstance 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/livedata/RequestPermissionLiveData.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode.livedata 2 | 3 | import androidx.activity.result.ActivityResultLauncher 4 | import androidx.activity.result.ActivityResultRegistry 5 | import androidx.activity.result.contract.ActivityResultContracts 6 | import androidx.lifecycle.LiveData 7 | 8 | class RequestPermissionLiveData( 9 | private val registry: ActivityResultRegistry, 10 | private val key: String 11 | ) : LiveData() { 12 | 13 | private lateinit var requestPermissionLauncher: ActivityResultLauncher 14 | 15 | override fun onActive() { 16 | requestPermissionLauncher = 17 | registry.register(key, ActivityResultContracts.RequestPermission()) { result -> 18 | value = result 19 | } 20 | } 21 | 22 | override fun onInactive() = requestPermissionLauncher.unregister() 23 | 24 | fun requestPermission(permission: String) { 25 | requestPermissionLauncher.launch(permission) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/livedata/TakePhotoLiveData.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode.livedata 2 | 3 | import android.graphics.Bitmap 4 | import androidx.activity.result.ActivityResultLauncher 5 | import androidx.activity.result.ActivityResultRegistry 6 | import androidx.activity.result.contract.ActivityResultContracts 7 | import androidx.lifecycle.LiveData 8 | 9 | class TakePhotoLiveData(private val registry: ActivityResultRegistry, private val key: String) : 10 | LiveData() { 11 | 12 | private lateinit var takePhotoLauncher: ActivityResultLauncher 13 | 14 | override fun onActive() { 15 | takePhotoLauncher = 16 | registry.register(key, ActivityResultContracts.TakePicturePreview()) { result -> 17 | value = result 18 | } 19 | } 20 | 21 | override fun onInactive() = takePhotoLauncher.unregister() 22 | 23 | fun takePhoto() = takePhotoLauncher.launch(null) 24 | 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/livedata/TimerGlobalLiveData.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode.livedata 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import androidx.annotation.MainThread 6 | import androidx.lifecycle.LiveData 7 | 8 | class TimerGlobalLiveData : LiveData() { 9 | 10 | private val handler: Handler = Handler(Looper.getMainLooper()) 11 | 12 | private val timerRunnable = object : Runnable { 13 | override fun run() { 14 | postValue(count++) 15 | handler.postDelayed(this, 1000) 16 | } 17 | } 18 | 19 | fun startTimer() { 20 | count = 0 21 | handler.postDelayed(timerRunnable, 1000) 22 | } 23 | 24 | fun cancelTimer() { 25 | handler.removeCallbacks(timerRunnable) 26 | } 27 | 28 | companion object { 29 | private lateinit var sInstance: TimerGlobalLiveData 30 | 31 | private var count = 0 32 | 33 | @MainThread 34 | fun get(): TimerGlobalLiveData { 35 | sInstance = if (Companion::sInstance.isInitialized) sInstance else TimerGlobalLiveData() 36 | return sInstance 37 | } 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/location/FusedLocationHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode.location 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.location.Location 6 | import android.os.Looper 7 | import android.util.Log 8 | import com.google.android.gms.location.LocationCallback 9 | import com.google.android.gms.location.LocationRequest 10 | import com.google.android.gms.location.LocationResult 11 | import com.google.android.gms.location.LocationServices 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.ExperimentalCoroutinesApi 14 | import kotlinx.coroutines.channels.awaitClose 15 | import kotlinx.coroutines.flow.* 16 | 17 | class FusedLocationHelper constructor(context: Context, externalScope: CoroutineScope) { 18 | 19 | private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) 20 | private val locationRequest = createLocationRequest() 21 | 22 | private val TAG = "SharedLocationManager" 23 | 24 | private fun createLocationRequest() = LocationRequest.create().apply { 25 | interval = 5000 26 | fastestInterval = 2000 27 | numUpdates = 1 28 | priority = LocationRequest.PRIORITY_HIGH_ACCURACY 29 | } 30 | 31 | @ExperimentalCoroutinesApi 32 | @SuppressLint("MissingPermission") 33 | private val _locationUpdates: SharedFlow = callbackFlow { 34 | val callback = object : LocationCallback() { 35 | override fun onLocationResult(result: LocationResult?) { 36 | result ?: return 37 | Log.d(TAG, "New location: ${result.lastLocation}") 38 | offer(result.lastLocation) 39 | } 40 | 41 | } 42 | Log.d(TAG, "Starting location updates") 43 | 44 | fusedLocationClient.requestLocationUpdates( 45 | locationRequest, 46 | callback, 47 | Looper.getMainLooper() 48 | ).addOnFailureListener { e -> 49 | close(e) 50 | } 51 | 52 | awaitClose { 53 | Log.d(TAG, "Stopping location updates") 54 | fusedLocationClient.removeLocationUpdates(callback) 55 | } 56 | }.shareIn( 57 | externalScope, 58 | replay = 0, 59 | started = SharingStarted.WhileSubscribed() 60 | ) 61 | 62 | @ExperimentalCoroutinesApi 63 | fun locationFlow(): Flow { 64 | return _locationUpdates 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/location/LocationHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode.location 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.location.Location 6 | import android.location.LocationListener 7 | import android.location.LocationManager 8 | import android.os.Build 9 | import android.os.Looper 10 | import kotlinx.coroutines.delay 11 | 12 | object LocationHelper { 13 | 14 | @SuppressLint("MissingPermission") 15 | suspend fun getLocation(context: Context, timeout: Long, callback: (location: Location?) -> Unit) { 16 | val locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager 17 | 18 | try { 19 | var bestLocation: Location? = null 20 | var hasSendResult = false 21 | 22 | if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { 23 | val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) 24 | if (isBetterLocation(location, bestLocation)) { 25 | bestLocation = location 26 | } 27 | } 28 | if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { 29 | val location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) 30 | if (isBetterLocation(location, bestLocation)) { 31 | bestLocation = location 32 | } 33 | } 34 | 35 | var gpsListener: LocationListener? 36 | var networkListener: LocationListener? = null 37 | object : LocationListener { 38 | override fun onLocationChanged(location: Location) { 39 | if (isBetterLocation(location, bestLocation)) { 40 | bestLocation = location 41 | } 42 | locationManager.removeUpdates(this) 43 | gpsListener = null 44 | if (bestLocation != null) { 45 | callback(bestLocation) 46 | hasSendResult = true 47 | networkListener?.let { 48 | locationManager.removeUpdates(it) 49 | } 50 | } 51 | } 52 | }.also { gpsListener = it } 53 | 54 | networkListener = object : LocationListener { 55 | override fun onLocationChanged(location: Location) { 56 | if (isBetterLocation(location, bestLocation)) { 57 | bestLocation = location 58 | } 59 | locationManager.removeUpdates(this) 60 | networkListener = null 61 | } 62 | } 63 | 64 | if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { 65 | gpsListener?.let { 66 | requestSingleUpdate(context, locationManager, LocationManager.GPS_PROVIDER, it) { 67 | callback(bestLocation) 68 | hasSendResult = true 69 | } 70 | } 71 | } 72 | 73 | if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { 74 | networkListener?.let { 75 | requestSingleUpdate(context, locationManager, LocationManager.NETWORK_PROVIDER, it) { 76 | callback(bestLocation) 77 | hasSendResult = true 78 | } 79 | } 80 | } 81 | 82 | delay(timeout) 83 | 84 | if (!hasSendResult) callback(bestLocation) 85 | 86 | gpsListener?.let { locationManager.removeUpdates(it) } 87 | 88 | networkListener?.let { locationManager.removeUpdates(it) } 89 | } catch (t: SecurityException) { 90 | callback(null) 91 | } 92 | } 93 | 94 | @SuppressLint("MissingPermission") 95 | private fun requestSingleUpdate(context: Context, locationManager: LocationManager, provider: String, locationListener: LocationListener, resultCallback: ((location: Location) -> Unit)? = null) { 96 | if (!locationManager.isProviderEnabled(provider)) { 97 | return 98 | } 99 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 100 | locationManager.getCurrentLocation(provider, null, context.mainExecutor) { location -> 101 | location?.let { 102 | resultCallback?.invoke(it) 103 | } 104 | 105 | } 106 | } else { 107 | locationManager.requestSingleUpdate(provider, locationListener, Looper.getMainLooper()) 108 | } 109 | } 110 | 111 | 112 | private fun isBetterLocation(location: Location?, currentBestLocation: Location?): Boolean { 113 | if (location == null) { 114 | return false 115 | } 116 | if (currentBestLocation == null) { 117 | // A new location is always better than no location 118 | return true 119 | } 120 | 121 | val TWO_MINUTES = 1000 * 60 * 2 122 | 123 | // Check whether the new location fix is newer or older 124 | val timeDelta = location.time - currentBestLocation.time 125 | val isSignificantlyNewer: Boolean = timeDelta > TWO_MINUTES 126 | val isSignificantlyOlder: Boolean = timeDelta < -TWO_MINUTES 127 | val isNewer = timeDelta > 0 128 | 129 | // If it's been more than two minutes since the current location, use 130 | // the new location 131 | // because the user has likely moved 132 | if (isSignificantlyNewer) { 133 | return true 134 | // If the new location is more than two minutes older, it must be 135 | // worse 136 | } else if (isSignificantlyOlder) { 137 | return false 138 | } 139 | 140 | // Check whether the new location fix is more or less accurate 141 | val accuracyDelta = (location.accuracy - currentBestLocation.accuracy).toInt() 142 | val isLessAccurate = accuracyDelta > 0 143 | val isMoreAccurate = accuracyDelta < 0 144 | val isSignificantlyLessAccurate = accuracyDelta > 200 145 | 146 | // Check if the old and new location are from the same provider 147 | val isFromSameProvider = location.provider == currentBestLocation.provider 148 | 149 | // Not significantly newer or older, so check for Accuracy 150 | if (isMoreAccurate) { 151 | // If more accurate return true 152 | return true 153 | } else if (isNewer && !isLessAccurate) { 154 | // Same accuracy but newer, return true 155 | return true 156 | } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) { 157 | // Accuracy is less (not much though) but is new, so if from same 158 | // provider return true 159 | return true 160 | } 161 | return false 162 | } 163 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/location/LocationHelperV2.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode.location 2 | 3 | import android.content.Context 4 | import android.location.Location 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.withTimeoutOrNull 8 | 9 | /** 10 | * 先获取网络定位 ,如果当前在室内,拿不到 gps 的情况下,设置2s 超时,如果拿的到 gps 位置,就返回最新的 gps 位置 11 | */ 12 | class LocationHelperV2(val context: Context, val externalScope: CoroutineScope) { 13 | 14 | suspend fun getLocation(): Location? { 15 | var location: Location? = 16 | NetWorkLocationHelper(context, externalScope).getNetLocation(context) 17 | withTimeoutOrNull(2000) { 18 | FusedLocationHelper(context, externalScope) 19 | .locationFlow() 20 | .collect { 21 | location = it 22 | } 23 | } 24 | return location 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/location/LocationPermissionUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode.location 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.content.pm.PackageManager 7 | import android.location.LocationManager 8 | import androidx.core.content.ContextCompat 9 | 10 | object LocationPermissionUtils { 11 | 12 | fun isLocationPermissionGranted(context: Context): Boolean { 13 | return (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) === PackageManager.PERMISSION_GRANTED 14 | || ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) === PackageManager.PERMISSION_GRANTED) 15 | } 16 | 17 | fun isLocationProviderEnabled(context: Context): Boolean { 18 | val locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager 19 | return (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) 20 | || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/location/NetWorkLocationHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode.location 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.location.Location 6 | import android.location.LocationListener 7 | import android.location.LocationManager 8 | import android.os.Build 9 | import android.os.Looper 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.ExperimentalCoroutinesApi 12 | import kotlinx.coroutines.channels.awaitClose 13 | import kotlinx.coroutines.flow.Flow 14 | import kotlinx.coroutines.flow.SharingStarted 15 | import kotlinx.coroutines.flow.callbackFlow 16 | import kotlinx.coroutines.flow.shareIn 17 | import kotlinx.coroutines.suspendCancellableCoroutine 18 | import kotlin.coroutines.resume 19 | 20 | class NetWorkLocationHelper(context: Context, externalScope: CoroutineScope) { 21 | 22 | private val locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager 23 | 24 | @SuppressLint("MissingPermission") 25 | suspend fun getNetLocation(context: Context, callback: (location: Location) -> Unit) { 26 | 27 | if (!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { 28 | return 29 | } 30 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 31 | locationManager.getCurrentLocation(LocationManager.NETWORK_PROVIDER, null, context.mainExecutor) { location -> 32 | callback.invoke(location) 33 | } 34 | } else { 35 | locationManager.requestSingleUpdate(LocationManager.NETWORK_PROVIDER, { location -> 36 | callback.invoke(location) 37 | }, null) 38 | } 39 | } 40 | 41 | 42 | @SuppressLint("MissingPermission") 43 | suspend fun getNetLocation(context: Context): Location? = suspendCancellableCoroutine { continuation -> 44 | if (!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { 45 | continuation.resume(null) 46 | } 47 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 48 | locationManager.getCurrentLocation(LocationManager.NETWORK_PROVIDER, null, context.mainExecutor) { location -> 49 | continuation.resume(location) 50 | } 51 | } else { 52 | locationManager.requestSingleUpdate(LocationManager.NETWORK_PROVIDER, { location -> 53 | continuation.resume(location) 54 | }, Looper.getMainLooper()) 55 | } 56 | } 57 | 58 | /** 59 | * 注意!不要在每个函数调用时创建新的实例 60 | * 切勿在调用某个函数调用返回时,使用 shareIn 或 stateIn 创建新的数据流。 61 | * 这样会在每次函数调用时创建一个新的 SharedFlow 或 StateFlow,而它们将会一直保持在内存中,直到作用域被取消或者在没有任何引用时被垃圾回收。 62 | */ 63 | @ExperimentalCoroutinesApi 64 | fun getNetLocationFlow(): Flow { 65 | return _locationUpdates 66 | } 67 | 68 | /** 69 | * 返回Flow流封装的,支持操作符,支持背压 70 | */ 71 | @ExperimentalCoroutinesApi 72 | @SuppressLint("MissingPermission") 73 | private val _locationUpdates = callbackFlow { 74 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 75 | locationManager.getCurrentLocation(LocationManager.NETWORK_PROVIDER, null, context.mainExecutor) { location -> 76 | offer(location) 77 | } 78 | awaitClose() 79 | } else { 80 | val locationListener = LocationListener { location -> offer(location) } 81 | locationManager.requestSingleUpdate(LocationManager.NETWORK_PROVIDER, locationListener, Looper.getMainLooper()) 82 | awaitClose { 83 | locationManager.removeUpdates(locationListener) 84 | } 85 | } 86 | }.shareIn( 87 | externalScope, 88 | replay = 0, 89 | started = SharingStarted.WhileSubscribed() 90 | ) 91 | 92 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/permission/PermissionKtx.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode.permission 2 | 3 | import androidx.activity.result.contract.ActivityResultContracts 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.core.app.ActivityCompat 6 | import androidx.fragment.app.Fragment 7 | 8 | inline fun Fragment.requestPermission( 9 | permission: String, 10 | crossinline granted: (permission: String) -> Unit = {}, 11 | crossinline denied: (permission: String) -> Unit = {}, 12 | crossinline explained: (permission: String) -> Unit = {} 13 | 14 | ) { 15 | 16 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { result: Boolean -> 17 | when { 18 | result -> granted.invoke(permission) 19 | shouldShowRequestPermissionRationale(permission) -> denied.invoke(permission) 20 | else -> explained.invoke(permission) 21 | } 22 | }.launch(permission) 23 | } 24 | 25 | inline fun AppCompatActivity.requestPermission( 26 | permission: String, 27 | crossinline granted: (permission: String) -> Unit = {}, 28 | crossinline denied: (permission: String) -> Unit = {}, 29 | crossinline explained: (permission: String) -> Unit = {} 30 | ) { 31 | 32 | 33 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { result -> 34 | when { 35 | result -> granted.invoke(permission) 36 | ActivityCompat.shouldShowRequestPermissionRationale(this, permission) -> denied.invoke( 37 | permission 38 | ) 39 | else -> explained.invoke(permission) 40 | } 41 | }.launch(permission) 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/commoncode/view/LifecycleView.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.commoncode.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.util.Log 6 | import android.view.View 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.lifecycle.LifecycleEventObserver 9 | import androidx.lifecycle.LifecycleOwner 10 | 11 | /** 12 | * see https://xuyisheng.top/lifecycle/ 13 | */ 14 | class LifecycleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, lifecycleOwner: LifecycleOwner) 15 | : View(context, attrs, defStyleAttr), LifecycleEventObserver { 16 | 17 | init { 18 | Log.i("wutao--> ", "init: ") 19 | lifecycleOwner.lifecycle.addObserver(this) 20 | } 21 | 22 | fun release() { 23 | Log.i("wutao--> ", "release") 24 | } 25 | 26 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 27 | when (event) { 28 | Lifecycle.Event.ON_DESTROY -> { 29 | release() 30 | source.lifecycle.removeObserver(this) 31 | } 32 | Lifecycle 33 | .Event.ON_RESUME -> { 34 | Log.i("wutao--> ", "ON_RESUME: ") 35 | } 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/res/color/color_md_contained_btn_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_md_contained_btn_stroke.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_md_contained_btn_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_md_dashed_btn_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_md_outlined_btn_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_md_outlined_btn_stroke.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_md_outlined_btn_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_md_text_btn_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_md_text_btn_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_button_dash_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_live_data_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 |