diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..c68d202 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +@davkutalek @jumaallan + +## code changes will send PR to following users +* @davkutalek @jumaallan diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..916d456 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +# Scope + +_Please explain what your feature does in a short paragraph._ please check the below boxes +- [ ] I have followed the coding conventions +- [ ] I have added/updated necessary tests +- [ ] I have tested the changes added on a physical device +- [ ] I have run `./codeAnalysis.sh` to make sure all lint/formatting checks have been done. + +## Closes/Fixes Issues +_Declare any issues by typing `fixes #1` or `closes #1` for example so that the automation can kick +in when this is merged_ + +## Other testing QA Notes +_What have you tested specifically and what possible impacts/areas there are that may need retesting +by others._ + +Please add a screenshot (if necessary) + diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 0000000..a176d97 --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,30 @@ +name: Build and test + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build_and_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Bundle install + run: bundle install + + - name: Static check + run: bundle exec fastlane staticCheck + + - name: Unit tests + run: bundle exec fastlane unitTest + + - name: Upload test report + uses: actions/upload-artifact@v3 + if: failure() + with: + name: junit_test_report_${{ env.GITHUB_REF }}_${{ env.GITHUB_RUN_ID }} + path: multisim/build/reports/tests diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7a118b4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..22e4779 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,218 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.5) + rexml + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + artifactory (3.0.15) + atomos (0.1.3) + aws-eventstream (1.2.0) + aws-partitions (1.642.0) + aws-sdk-core (3.158.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.525.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.58.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.114.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.5.2) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.4) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.93.0) + faraday (1.10.2) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.2.6) + fastlane (2.210.1) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (~> 2.0.0) + naturally (~> 2.2) + optparse (~> 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.29.0) + google-apis-core (>= 0.9.0, < 2.a) + google-apis-core (0.9.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.15.0) + google-apis-core (>= 0.9.0, < 2.a) + google-apis-playcustomapp_v1 (0.11.0) + google-apis-core (>= 0.9.0, < 2.a) + google-apis-storage_v1 (0.19.0) + google-apis-core (>= 0.9.0, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.0) + google-cloud-storage (1.43.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.19.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.2.0) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.5) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.1) + json (2.6.2) + jwt (2.5.0) + memoist (0.16.2) + mini_magick (4.11.0) + mini_mime (1.1.2) + multi_json (1.15.0) + multipart-post (2.0.0) + nanaimo (0.3.0) + naturally (2.2.1) + optparse (0.1.1) + os (1.1.4) + plist (3.6.0) + public_suffix (5.0.0) + rake (13.0.6) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.5) + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + signet (0.17.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.8) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.8.2) + unicode-display_width (1.8.0) + webrick (1.7.0) + word_wrap (1.0.0) + xcodeproj (1.22.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-21 + +DEPENDENCIES + fastlane + +BUNDLED WITH + 2.3.11 diff --git a/app/.gitignore b/app/.gitignore index 796b96d..42afabf 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1 @@ -/build +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index b2d063e..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - defaultConfig { - minSdkVersion 18 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' - implementation "androidx.work:work-runtime:2.2.0" - - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - - implementation 'io.sentry:sentry-android:1.7.21' -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..6879c65 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,83 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jlleitschuh.gradle.ktlint") + id("io.gitlab.arturbosch.detekt") +} + +android { + + namespace = "com.hover.app" + + compileSdk = 33 + + defaultConfig { + applicationId = "com.hover.app" + minSdk = 21 + targetSdk = 33 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + buildFeatures { + compose = true + } + + packagingOptions { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.2.0" + } +} + +dependencies { + + implementation(project(":multisim")) + + implementation(libs.android.coreKtx) + implementation(libs.android.appcompat) + implementation(libs.android.material) + + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1") + implementation("androidx.activity:activity-compose:1.6.0") + implementation("androidx.compose.ui:ui:1.2.1") + implementation("androidx.compose.ui:ui-tooling-preview:1.2.1") + implementation("androidx.compose.material:material:1.2.1") + + androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.2.1") + debugImplementation("androidx.compose.ui:ui-tooling:1.2.1") + debugImplementation("androidx.compose.ui:ui-test-manifest:1.2.1") + + androidTestImplementation(libs.test.android.junit) + androidTestImplementation(libs.test.android.espresso) + + testImplementation(libs.test.junit4) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/hover/app/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/hover/app/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..93ba096 --- /dev/null +++ b/app/src/androidTest/java/com/hover/app/ExampleInstrumentedTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.app + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.hover.app", appContext.packageName) + } +} diff --git a/app/src/androidTest/java/com/hover/multisim/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/hover/multisim/ExampleInstrumentedTest.java deleted file mode 100644 index 5bfb06c..0000000 --- a/app/src/androidTest/java/com/hover/multisim/ExampleInstrumentedTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.hover.multisim; - -import android.content.Context; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - - assertEquals("com.hover.multisim", appContext.getPackageName()); - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ec7315a..6610b74 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,9 +1,28 @@ - - - - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/hover/app/MainActivity.kt b/app/src/main/java/com/hover/app/MainActivity.kt new file mode 100644 index 0000000..cee7a36 --- /dev/null +++ b/app/src/main/java/com/hover/app/MainActivity.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.app + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.hover.app.ui.theme.MultiSimTheme + +class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MultiSimTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colors.background + ) { + Greeting("Android") + } + } + } + } +} + +@Composable +fun Greeting(name: String) { + Text(text = "Hello $name!") +} + +@Preview(showBackground = true) +@Composable +fun DefaultPreview() { + MultiSimTheme { + Greeting("Android") + } +} diff --git a/app/src/main/java/com/hover/app/ui/theme/Color.kt b/app/src/main/java/com/hover/app/ui/theme/Color.kt new file mode 100644 index 0000000..a8b19d8 --- /dev/null +++ b/app/src/main/java/com/hover/app/ui/theme/Color.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.app.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple200 = Color(0xFFBB86FC) +val Purple500 = Color(0xFF6200EE) +val Purple700 = Color(0xFF3700B3) +val Teal200 = Color(0xFF03DAC5) diff --git a/app/src/main/java/com/hover/app/ui/theme/Shape.kt b/app/src/main/java/com/hover/app/ui/theme/Shape.kt new file mode 100644 index 0000000..5516a33 --- /dev/null +++ b/app/src/main/java/com/hover/app/ui/theme/Shape.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.app.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) +) diff --git a/app/src/main/java/com/hover/app/ui/theme/Theme.kt b/app/src/main/java/com/hover/app/ui/theme/Theme.kt new file mode 100644 index 0000000..5286226 --- /dev/null +++ b/app/src/main/java/com/hover/app/ui/theme/Theme.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.app.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable + +private val DarkColorPalette = darkColors( + primary = Purple200, + primaryVariant = Purple700, + secondary = Teal200 +) + +private val LightColorPalette = lightColors( + primary = Purple500, + primaryVariant = Purple700, + secondary = Teal200 + + /* Other default colors to override + background = Color.White, + surface = Color.White, + onPrimary = Color.White, + onSecondary = Color.Black, + onBackground = Color.Black, + onSurface = Color.Black, + */ +) + +@Composable +fun MultiSimTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colors = colors, + typography = Typography, + shapes = Shapes, + content = content + ) +} diff --git a/app/src/main/java/com/hover/app/ui/theme/Type.kt b/app/src/main/java/com/hover/app/ui/theme/Type.kt new file mode 100644 index 0000000..81d8253 --- /dev/null +++ b/app/src/main/java/com/hover/app/ui/theme/Type.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.app.ui.theme + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + body1 = TextStyle( + fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp + ), + button = TextStyle( + fontFamily = FontFamily.Default, fontWeight = FontWeight.W500, fontSize = 14.sp + ), + caption = TextStyle( + fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 12.sp + ) +) diff --git a/app/src/main/java/com/hover/multisim/DataSource.java b/app/src/main/java/com/hover/multisim/DataSource.java deleted file mode 100644 index 3f94a3a..0000000 --- a/app/src/main/java/com/hover/multisim/DataSource.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.hover.multisim; - -import android.content.Context; -import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; - -public class DataSource { - public final static String TAG = "DataSource"; - - private Context context; - protected SQLiteDatabase database; - - protected DataSource(Context context) { context = context.getApplicationContext(); } - - protected void open() throws SQLException { - database = SimDatabase.getInstance(context).getWritableDatabase(); - } - @SuppressWarnings("EmptyMethod") - protected void close() throws SQLException { - // Apparently don't need to do this? Seems strange but: https://stackoverflow.com/questions/6608498/best-place-to-close-database-connection - } -} diff --git a/app/src/main/java/com/hover/multisim/MultiSimWorker.java b/app/src/main/java/com/hover/multisim/MultiSimWorker.java deleted file mode 100644 index 1fa151f..0000000 --- a/app/src/main/java/com/hover/multisim/MultiSimWorker.java +++ /dev/null @@ -1,432 +0,0 @@ -package com.hover.multisim; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Build; -import android.os.SystemClock; -import androidx.annotation.NonNull; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import android.telephony.PhoneStateListener; -import android.telephony.ServiceState; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; -import android.util.Log; - -import com.google.common.util.concurrent.ListenableFuture; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; - -import androidx.work.ListenableWorker; -import androidx.work.OneTimeWorkRequest; -import androidx.work.PeriodicWorkRequest; -import androidx.work.WorkerParameters; -import androidx.work.impl.utils.futures.SettableFuture; -import io.sentry.Sentry; - -final public class MultiSimWorker extends ListenableWorker { - public static final String TAG = "MultiSimTeleMgr"; - private static final String NEW_SIM_INFO = "NEW_SIM_INFO_ACTION"; - static final int SLOT_COUNT = 3; // Need to check 0, 1, and 2. Some phones index from 1. - - private SettableFuture workerFuture; - private Result result = null; - - private final Semaphore slotSemaphore = new Semaphore(1, true); - private final Semaphore simSemaphore = new Semaphore(1, true); - - private SimStateReceiver simStateReceiver; - private SimStateListener simStateListener; - private ArrayList validClassNames; - - private final String[] POSS_CLASS_NAMES = new String[] { - null, - "android.telephony.TelephonyManager", - "android.telephony.MSimTelephonyManager", - "android.telephony.MultiSimTelephonyService", - "com.mediatek.telephony.TelephonyManagerEx", - "com.android.internal.telephony.Phone", - "com.android.internal.telephony.PhoneFactory" - }; - - public MultiSimWorker(@NonNull Context context, @NonNull WorkerParameters params) { - super(context, params); - } - - public static PeriodicWorkRequest makeToil() { - return new PeriodicWorkRequest.Builder(MultiSimWorker.class, 15, TimeUnit.MINUTES).build(); - } - - public static OneTimeWorkRequest makeWork() { - return new OneTimeWorkRequest.Builder(MultiSimWorker.class).build(); - } - - @Override - @SuppressLint("RestrictedApi") - public @NonNull ListenableFuture startWork() { - Log.v(TAG, "Starting new Multi SIM worker"); - workerFuture = SettableFuture.create(); - - if (Utils.hasPhonePerm(getApplicationContext())) - startListeners(); - else - workerFuture.set(Result.failure()); - return workerFuture; - } - - @SuppressLint("RestrictedApi") - private void startListeners() { - try { - registerSimStateReceiver(); - if (simStateListener == null) simStateListener = new SimStateListener(); - // TelephonyManager.listen() must take place on the main thread - ((TelephonyManager) getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE)) - .listen(simStateListener, PhoneStateListener.LISTEN_SERVICE_STATE | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS); - } catch (Exception e) { - Log.d(TAG, "Failed to start SIM listeners, setting retry", e); - workerFuture.set(Result.retry()); - } - } - - @SuppressLint("RestrictedApi") - synchronized private void updateSimInfo() { - getBackgroundExecutor().execute(new Runnable() { - @Override - public void run() { - try { - simSemaphore.acquire(); - if (Utils.hasPhonePerm(getApplicationContext())) { - Log.v(TAG, "reviewing sim info"); - List oldList = getSaved(); - List newList = findUniqueSimInfo(); - - if (newList != null) { - compareNewAndOld(newList, oldList); - result = Result.success(); - } else - result = Result.failure(); - } else result = Result.failure(); - } catch (Exception e) { Log.w(TAG, "threw while attempting to update sim list", e); Sentry.capture(e); result = Result.failure(); - } finally { - simSemaphore.release(); -// Give the listeners a chance to receive a few events - sometimes the first trigger isn't the needed info. 5s is long but this is a background thread anyway and the info has already been updated in the DB - SystemClock.sleep(5000); - if (!workerFuture.isDone()) { - Log.v(TAG, "Finishing Multi SIM worker"); - workerFuture.set(result); - } - } - } - }); - } - - private void compareNewAndOld(List newList, List oldList) { - if (oldList == null || oldList.size() != newList.size()) { - Log.v(TAG, "no old list or sizes differ. Old: " + (oldList != null ? oldList.size() : "null") + ", new: " + newList.size()); - onSimInfoUpdate(newList); - } else { - for (int i = 0; i < newList.size(); i++) - if (newList.get(i).isNotContainedInOrHasMoved(oldList)) { - Log.v(TAG, "some sim moved"); - onSimInfoUpdate(newList); - break; - } - } - } - - private ArrayList getSaved() { - ArrayList oldList = null; - for (int i = 0; i < SLOT_COUNT; i++) { - SimInfo si = new SimDataSource(getApplicationContext()).get(i); - if (si != null) { - if (oldList == null) oldList = new ArrayList<>(); - oldList.add(si); - } - } - Log.v(TAG, "Loaded old list from db. Size: " + (oldList != null ? oldList.size() : "null")); - return oldList; - } - - private void onSimInfoUpdate(List newList) { - updateDb(newList); - Log.v(TAG, "Saved. Firing broadcast"); - LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(new Intent(action(getApplicationContext()))); - } - - private void updateDb(List newList) { - for (SimInfo si: newList) { - Log.i(TAG, "Saving SIM in slot " + si.slotIdx + ": " + si.toString()); - si.save(getApplicationContext()); - } - - List dbInfos = new SimDataSource(getApplicationContext()).getAll(); - for (SimInfo dbSi: dbInfos) { - if (!findPhysicalSim(newList, dbSi)) { - Log.i(TAG, "Couldn't find SIM: " + dbSi.toString() + ", removing"); - dbSi.setSimRemoved(getApplicationContext()); - } - } - } - - private boolean findPhysicalSim(List newList, SimInfo savedSim) { - for (SimInfo si: newList) { - if (si.isSameSim(savedSim)) - return true; - } - return false; - } - - @SuppressWarnings({"MissingPermission"}) - synchronized private List findUniqueSimInfo() { - List newList = new ArrayList<>(); - try { - slotSemaphore.acquire(); - List slotMgrList = new ArrayList<>(); - List teleMgrInstances = listTeleMgrs(slotMgrList); - - List subInfos = new ArrayList<>(); - if (Build.VERSION.SDK_INT >= 22) - subInfos = getSubscriptions(teleMgrInstances, slotMgrList); - - if (subInfos != null && subInfos.size() > 0) - newList = createUniqueSimInfoList(subInfos, slotMgrList); - else - newList = createUniqueSimInfoList(slotMgrList); - } catch (Exception e) { Log.w(TAG, "Multi-SIM worker caught something", e); Sentry.capture(e); - } finally { slotSemaphore.release(); } - return newList; - } - - private List createUniqueSimInfoList(List subInfos, List slotMgrList) { - List newList = createUniqueSimInfoList(slotMgrList); - for (SubscriptionInfo subInfo: subInfos) - newList.add(new SimInfo(subInfo, getApplicationContext())); - return removeDuplicates(newList); - } - private List createUniqueSimInfoList(List slotMgrList) { - List newList = new ArrayList<>(); - for (SlotManager sm: slotMgrList) - newList.add(sm.createSimInfo()); - return removeDuplicates(newList); - } - private List removeDuplicates(List simInfos) { - List uniqueSimInfos = new ArrayList<>(); - for (SimInfo simInfo: simInfos) - if (simInfo.isNotContainedIn(uniqueSimInfos)) - uniqueSimInfos.add(simInfo); - return uniqueSimInfos; - } - - @TargetApi(22) - @SuppressWarnings({"MissingPermission"}) - private List getSubscriptions(List teleMgrInstances, List slotMgrList) throws Exception { - List subInfos = SubscriptionManager.from(getApplicationContext()).getActiveSubscriptionInfoList(); - if (teleMgrInstances != null) { - for (Object teleMgr : teleMgrInstances) { - if (subInfos != null) { - for (SubscriptionInfo subinfo : subInfos) - SlotManager.addValidReadySlots(slotMgrList, subinfo.getSimSlotIndex(), subinfo.getSubscriptionId(), teleMgr, validClassNames); - } - } - } - return subInfos; - } - - @SuppressWarnings("ResourceType") - private List listTeleMgrs(List slotMgrList) { - if (validClassNames == null || validClassNames.isEmpty()) validClassNames = new ArrayList<>(Arrays.asList(POSS_CLASS_NAMES)); - List teleMgrList = new ArrayList<>(); - -// Log("Creating TeleMgrs List.............................................................."); - for (String className : POSS_CLASS_NAMES) { - if (className == null) continue; - addMgrFromReflection(className, teleMgrList, null, slotMgrList); - for (int i = 0; i < SLOT_COUNT; i++) - addMgrFromReflection(className, teleMgrList, i, slotMgrList); - } - - addMgrFromSystemService(Context.TELEPHONY_SERVICE, teleMgrList, null, slotMgrList); - addMgrFromSystemService("phone_msim", teleMgrList, null, slotMgrList); - for (int j = 0; j < SLOT_COUNT; j++) - addMgrFromSystemService("phone" + j, teleMgrList, j, slotMgrList); - teleMgrList.add(null); - -// Log("Total TeleMgrInstances length including null entry: " + teleMgrList.size()); -// Log("Valid class names were: " + validClassNames.toString()); -// Log("...................................................................................."); - return teleMgrList; - } - private void addMgrFromReflection(String className, List teleMgrList, Object slotIdx, List slotMgrList) { - Object result = runMethodReflect(className, "getDefault", slotIdx == null ? null : new Object[]{ slotIdx }); - if (result != null && !teleMgrList.contains(result)) { - teleMgrList.add(result); - Log("Added Mgr using className: " + className + ", method: getDefault, and param: " + slotIdx); - if (Build.VERSION.SDK_INT < 22) - SlotManager.addValidReadySlots(slotMgrList, slotIdx, result, validClassNames); - } - } - private void addMgrFromSystemService(String serviceName, List teleMgrList, Object slotIdx, List slotMgrList) { - Object serv = getApplicationContext().getSystemService(serviceName); - if (serv != null && !teleMgrList.contains(serv)) { - teleMgrList.add(serv); - Log("Added Mgr using mContext.getSystemService('" + serviceName + "')"); - if (Build.VERSION.SDK_INT < 22) - SlotManager.addValidReadySlots(slotMgrList, slotIdx, serv, validClassNames); - } - } - - @SuppressWarnings("SameParameterValue") - private Object runMethodReflect(String className, String methodName, Object[] methodParams) { - try { - return runMethodReflect(null, Class.forName(className), methodName, methodParams); - } catch (ClassNotFoundException e) { - validClassNames.remove(className); -// Log("Class not found, removing: " + e); - } - return null; - } - private static Object runMethodReflect(Object actualInstance, String methodName, Object[] methodParams) { - return runMethodReflect(actualInstance, actualInstance.getClass(), methodName, methodParams); - } - public static Object runMethodReflect(Object actualInstance, Class classInstance, String methodName, Object[] methodParams) { - Object result = null; - try { - Method method = classInstance.getDeclaredMethod(methodName, getClassParams(methodParams)); - boolean accessible = method.isAccessible(); - method.setAccessible(true); - result = method.invoke(actualInstance != null ? actualInstance : classInstance, methodParams); - method.setAccessible(accessible); - } catch (Exception ignored) { /* Log("Method not found: " + ignored); */ } - return result; - } - - @SuppressWarnings("unused") - private static Object runFieldReflect(String className, String field) { - Object result = null; - try { - Class classInstance = Class.forName(className); - Field fieldReflect = classInstance.getField(field); - boolean accessible = fieldReflect.isAccessible(); - fieldReflect.setAccessible(true); - result = fieldReflect.get(null).toString(); - fieldReflect.setAccessible(accessible); - } catch (Exception ignored) { -// Log("Error accessing reflected class: " + ignored); - } - return result; - } - - private static Class[] getClassParams(Object[] methodParams) { - Class[] classesParams = null; - if (methodParams != null) { - classesParams = new Class[methodParams.length]; - for (int i = 0; i < methodParams.length; i++) { - if (methodParams[i] instanceof Integer) - classesParams[i] = int.class; // logString += methodParams[i] + ","; - else if (methodParams[i] instanceof String) - classesParams[i] = String.class; // logString += "\"" + methodParams[i] + "\","; - else if (methodParams[i] instanceof Long) - classesParams[i] = long.class; // logString += methodParams[i] + ","; - else if (methodParams[i] instanceof Boolean) - classesParams[i] = boolean.class; // logString += methodParams[i] + ","; - else - classesParams[i] = methodParams[i].getClass(); // logString += "["+methodParams[i]+"]" + ","; - } - } - return classesParams; - } - - private void registerSimStateReceiver() { - if (simStateReceiver == null) { - simStateReceiver = new SimStateReceiver(); - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction("android.intent.action.SIM_STATE_CHANGED"); - intentFilter.addAction("android.intent.action.ACTION_SIM_STATE_CHANGED"); - intentFilter.addAction("android.intent.action.PHONE_STATE"); - intentFilter.addAction("vivo.intent.action.ACTION_SIM_STATE_CHANGED"); - getApplicationContext().registerReceiver(simStateReceiver, intentFilter); - } - } - - private class SimStateListener extends PhoneStateListener { - public void onServiceStateChanged(ServiceState serviceState) { - updateSimInfo(); - } - } - - private class SimStateReceiver extends BroadcastReceiver { - @Override - public void onReceive(final Context context, final Intent intent) { - if (intent != null) updateSimInfo(); - } - } - - public static String action(Context c) { return Utils.getPackage(c) + "." + NEW_SIM_INFO; } - - @Override - public void onStopped() { - super.onStopped(); - try { - ((TelephonyManager) getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE)) - .listen(simStateListener, PhoneStateListener.LISTEN_NONE); - simStateListener = null; - if (simStateReceiver != null) - getApplicationContext().unregisterReceiver(simStateReceiver); - simStateReceiver = null; - } catch(Exception ignored) {} - } - - @SuppressWarnings("unused") - private void goFish() { - printAllMethodsAndFields("android.telephony.TelephonyManager", -1); // all methods - printAllMethodsAndFields("android.telephony.MultiSimTelephonyService", -1); // all methods -// printAllMethodsAndFields("android.telephony.MultiSimTelephonyService", 0); // methods with 0 params -// printAllMethodsAndFields("android.telephony.MultiSimTelephonyService", 1); // methods with 1 params -// printAllMethodsAndFields("android.telephony.MultiSimTelephonyService", 2); // methods with 2 params - printAllMethodsAndFields("android.telephony.MSimTelephonyManager", -1); // all methods - printAllMethodsAndFields("com.mediatek.telephony.TelephonyManager", -1); // all methods - printAllMethodsAndFields("com.mediatek.telephony.TelephonyManagerEx", -1); // all methods - printAllMethodsAndFields("com.android.internal.telephony.ITelephony", -1); // all methods - printAllMethodsAndFields("com.android.internal.telephony.ITelephony$Stub$Proxy", -1); // all methods - } - @SuppressWarnings("unused") - private void printAllMethodsAndFields(String className, int paramsCount) { - Log("===================================================================================="); - Log("Methods of " + className); - try { - Class MultiSimClass = Class.forName(className); - for (Method method : MultiSimClass.getMethods()) { -// if (method.toGenericString().toLowerCase().contains("ussd")) { - Log(method.toGenericString()); - try { - if (method.getParameterTypes().length == 0) - Log((String) runMethodReflect(MultiSimClass, method.getName(), null)); - else if (method.getParameterTypes().length == 1) - Log(" " + runMethodReflect(MultiSimClass, method.getName(), new Object[]{0})); - } catch (Exception e) { Log("Failed. " + e); } -// } - } - } catch (Exception e) { - Log("Failed. " + e); - } -// for( Field field : MultiSimClass.getFields()) { -// field.setAccessible(true); -// Log.i(LOG, " f2 " + field.getName() + " " + field.getType() + " " + field.load(inst)); -// } - } - - @SuppressWarnings({"EmptyMethod", "unused"}) - private static void Log(String message) { -// Log.e(TAG, message); - } -} diff --git a/app/src/main/java/com/hover/multisim/SimContract.java b/app/src/main/java/com/hover/multisim/SimContract.java deleted file mode 100644 index f0ba92b..0000000 --- a/app/src/main/java/com/hover/multisim/SimContract.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.hover.multisim; - -final public class SimContract { - public static final String TABLE_NAME = "hsdk_sims"; - - public static final String COLUMN_ENTRY_ID = "_id"; - - public static final String COLUMN_SLOT_IDX = "slot_idx"; - public static final String COLUMN_SUB_ID = "sub_id"; - public static final String COLUMN_IMEI = "imei"; - public static final String COLUMN_STATE = "state"; - - public static final String COLUMN_IMSI = "imsi"; - public static final String COLUMN_MCC = "mcc"; - public static final String COLUMN_MNC = "mnc"; - public static final String COLUMN_ICCID = "iccid"; - public static final String COLUMN_OP = "operator"; - public static final String COLUMN_OP_NAME = "operator_name"; - public static final String COLUMN_COUNTRY_ISO = "country_iso"; - public static final String COLUMN_ROAMING = "is_roaming"; - - public static final String COLUMN_NETWORK_CODE = "network_code"; - public static final String COLUMN_NETWORK_NAME = "network_name"; - public static final String COLUMN_NETWORK_COUNTRY = "network_country"; - public static final String COLUMN_NETWORK_TYPE = "network_type"; - - public static final String[] allColumns = { - COLUMN_ENTRY_ID, - COLUMN_SLOT_IDX, - COLUMN_SUB_ID, - COLUMN_IMEI, - COLUMN_STATE, - - COLUMN_IMSI, - COLUMN_MCC, - COLUMN_MNC, - COLUMN_ICCID, - COLUMN_OP, - COLUMN_OP_NAME, - COLUMN_COUNTRY_ISO, - COLUMN_ROAMING, - - COLUMN_NETWORK_CODE, - COLUMN_NETWORK_NAME, - COLUMN_NETWORK_COUNTRY, - COLUMN_NETWORK_TYPE - }; -} diff --git a/app/src/main/java/com/hover/multisim/SimDataSource.java b/app/src/main/java/com/hover/multisim/SimDataSource.java deleted file mode 100644 index a54df6d..0000000 --- a/app/src/main/java/com/hover/multisim/SimDataSource.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.hover.multisim; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; - -import io.sentry.Sentry; - -final public class SimDataSource extends DataSource { - private final static String TAG = "SimDataSource"; - private final String TABLE = SimContract.TABLE_NAME; - private final String[] COLUMNS = SimContract.allColumns; - - public SimDataSource(Context context) { super(context); } - - @SuppressWarnings("UnusedReturnValue") - long saveToDb(SimInfo simInfo) { - ContentValues cv = getContentValues(simInfo); - open(); - long insertId = database.insert(TABLE, null, cv); - close(); - Log.d(TAG, "Saved Sim with imsi: " + simInfo.imsi + ", iccid: " + simInfo.iccId + ". Id: " + insertId); - return insertId; - } - - public List getAll() { - List infos = new ArrayList<>(); - try { - open(); - Cursor cursor = database.query(TABLE, COLUMNS, null, null, null, null, null); - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - SimInfo si = cursorToSimInfo(cursor); - infos.add(si); - cursor.moveToNext(); - } - cursor.close(); - close(); - } catch (Exception e) { Sentry.capture(e); } - return infos; - } - - public SimInfo get(int slotIdx) { - return load(SimContract.COLUMN_SLOT_IDX + " = " + slotIdx); - } - @SuppressWarnings("unused") - public SimInfo loadBy(String iccId) { - return load(SimContract.COLUMN_ICCID + " = '" + iccId + "'"); - } - private SimInfo load(String selection) { - open(); - SimInfo si = null; - Cursor cursor = database.query(TABLE, COLUMNS, selection, null, null, null, null); - cursor.moveToFirst(); - if (!cursor.isAfterLast()) - si = cursorToSimInfo(cursor); - else - Log.d(TAG, "didn't load cursor..."); - - cursor.close(); - close(); - return si; - } - - @SuppressWarnings("UnnecessaryUnboxing") - public List getPresent(String mcc, String mnc) { - List matchingSims = new ArrayList<>(); - open(); - Cursor cursor = database.query(TABLE, COLUMNS, SimContract.COLUMN_MCC + " = '" + mcc + "' AND " + SimContract.COLUMN_SLOT_IDX + " != -1", null, null, null, null); - cursor.moveToFirst(); - int mncInt = Integer.valueOf(mnc).intValue(); - while (!cursor.isAfterLast()) { - SimInfo si = cursorToSimInfo(cursor); - if (si.isMncMatch(mncInt)) - matchingSims.add(si); - cursor.moveToNext(); - } - cursor.close(); - close(); - return matchingSims; - } - - void remove(SimInfo si) { - ContentValues cv = new ContentValues(); - cv.put(SimContract.COLUMN_SLOT_IDX, -1); - update(cv, si.iccId); - } - -// void updateSlot(SimInfo si, SimInfo updatedInfo) { -// ContentValues cv = new ContentValues(); -// if (updatedInfo != null) { -// cv.put(SimContract.COLUMN_SLOT_IDX, updatedInfo.slotIdx); -// cv.put(SimContract.COLUMN_SUB_ID, updatedInfo.subscriptionId); -// cv.put(SimContract.COLUMN_IMEI, updatedInfo.imei); -// cv.put(SimContract.COLUMN_STATE, updatedInfo.simState); -// } else -// cv.put(SimContract.COLUMN_SLOT_IDX, -1); -// -// update(cv, si.iccId); -// } - -// void updateNetwork(SimInfo si, String operator, String name, String countryIso, int type, int roaming) { -// ContentValues cv = new ContentValues(); -// cv.put(SimContract.COLUMN_NETWORK_CODE, operator); -// cv.put(SimContract.COLUMN_NETWORK_NAME, name); -// cv.put(SimContract.COLUMN_NETWORK_COUNTRY, countryIso); -// cv.put(SimContract.COLUMN_NETWORK_TYPE, type); -// cv.put(SimContract.COLUMN_ROAMING, roaming); -// -// update(cv, si.iccId); -// } - - private void update(ContentValues cv, String iccId) { - open(); - database.update(TABLE, cv, SimContract.COLUMN_ICCID + " = '" + iccId + "'", null); - close(); - } - - private ContentValues getContentValues(SimInfo simInfo) { - ContentValues cv = new ContentValues(); - cv.put(SimContract.COLUMN_SLOT_IDX, simInfo.slotIdx); - cv.put(SimContract.COLUMN_SUB_ID, simInfo.subscriptionId); - cv.put(SimContract.COLUMN_IMEI, simInfo.imei); - cv.put(SimContract.COLUMN_STATE, simInfo.simState); - - cv.put(SimContract.COLUMN_IMSI, simInfo.imsi); - cv.put(SimContract.COLUMN_MCC, simInfo.mcc); - cv.put(SimContract.COLUMN_MNC, simInfo.mnc); - cv.put(SimContract.COLUMN_ICCID, simInfo.setStandardIccId(simInfo.iccId)); - cv.put(SimContract.COLUMN_OP, simInfo.hni); - cv.put(SimContract.COLUMN_OP_NAME, simInfo.operatorName); - cv.put(SimContract.COLUMN_COUNTRY_ISO, simInfo.countryIso); - cv.put(SimContract.COLUMN_ROAMING, simInfo.networkRoaming ? 1 : 0); - - cv.put(SimContract.COLUMN_NETWORK_CODE, simInfo.networkOperator); - cv.put(SimContract.COLUMN_NETWORK_NAME, simInfo.networkOperatorName); - cv.put(SimContract.COLUMN_NETWORK_COUNTRY, simInfo.networkCountryIso); - cv.put(SimContract.COLUMN_NETWORK_TYPE, simInfo.networkType); - return cv; - } - - private SimInfo cursorToSimInfo(Cursor c) { - SimInfo simInfo = new SimInfo(); - simInfo.slotIdx = c.getInt(c.getColumnIndex(SimContract.COLUMN_SLOT_IDX)); - simInfo.subscriptionId = c.getInt(c.getColumnIndex(SimContract.COLUMN_SUB_ID)); - simInfo.imei = c.getString(c.getColumnIndex(SimContract.COLUMN_IMEI)); - simInfo.simState = c.getInt(c.getColumnIndex(SimContract.COLUMN_STATE)); - - simInfo.imsi = c.getString(c.getColumnIndex(SimContract.COLUMN_IMSI)); - simInfo.mcc = c.getString(c.getColumnIndex(SimContract.COLUMN_MCC)); - simInfo.mnc = c.getString(c.getColumnIndex(SimContract.COLUMN_MNC)); - simInfo.iccId = c.getString(c.getColumnIndex(SimContract.COLUMN_ICCID)); - simInfo.hni = c.getString(c.getColumnIndex(SimContract.COLUMN_OP)); - simInfo.operatorName = c.getString(c.getColumnIndex(SimContract.COLUMN_OP_NAME)); - simInfo.countryIso = c.getString(c.getColumnIndex(SimContract.COLUMN_COUNTRY_ISO)); - simInfo.networkRoaming = c.getInt(c.getColumnIndex(SimContract.COLUMN_ROAMING)) == 1; - - simInfo.networkOperator = c.getString(c.getColumnIndex(SimContract.COLUMN_NETWORK_CODE)); - simInfo.networkOperatorName = c.getString(c.getColumnIndex(SimContract.COLUMN_NETWORK_NAME)); - simInfo.networkCountryIso = c.getString(c.getColumnIndex(SimContract.COLUMN_NETWORK_COUNTRY)); - simInfo.networkType = c.getInt(c.getColumnIndex(SimContract.COLUMN_NETWORK_TYPE)); - return simInfo; - } -} diff --git a/app/src/main/java/com/hover/multisim/SimDatabase.java b/app/src/main/java/com/hover/multisim/SimDatabase.java deleted file mode 100644 index 522c999..0000000 --- a/app/src/main/java/com/hover/multisim/SimDatabase.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.hover.multisim; - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; - -public class SimDatabase extends SQLiteOpenHelper { - private static final int DATABASE_VERSION = 1; - private static final String DATABASE_NAME = "multisim.db"; - - private static SimDatabase instance = null; - - public static synchronized SimDatabase getInstance(Context ctx) { - if (instance == null) - instance = new SimDatabase(ctx.getApplicationContext()); - return instance; - } - - private SimDatabase(Context ctx) { - super(ctx, DATABASE_NAME, null, DATABASE_VERSION); - } - - private static final String SIM_TABLE_CREATE = "create table " - + SimContract.TABLE_NAME + "(" - + SimContract.COLUMN_ENTRY_ID + " integer primary key autoincrement, " - + SimContract.COLUMN_SLOT_IDX + " integer not null, " - + SimContract.COLUMN_SUB_ID + " integer not null, " - + SimContract.COLUMN_IMEI + " text, " - + SimContract.COLUMN_STATE + " integer default -1, " - + SimContract.COLUMN_IMSI + " text not null, " - + SimContract.COLUMN_MCC + " text not null, " - + SimContract.COLUMN_MNC + " text, " - + SimContract.COLUMN_ICCID + " text not null, " - + SimContract.COLUMN_OP + " text, " - + SimContract.COLUMN_OP_NAME + " text, " - + SimContract.COLUMN_COUNTRY_ISO + " text, " - + SimContract.COLUMN_ROAMING + " integer default 0 not null, " - + SimContract.COLUMN_NETWORK_CODE + " text, " - + SimContract.COLUMN_NETWORK_NAME + " text, " - + SimContract.COLUMN_NETWORK_COUNTRY + " text, " - + SimContract.COLUMN_NETWORK_TYPE + " integer, " - + "UNIQUE (" + SimContract.COLUMN_ICCID + ") ON CONFLICT REPLACE" - + ")"; - - public void onCreate(SQLiteDatabase db) { - db.execSQL(SIM_TABLE_CREATE); - } - - private static final String SQL_DELETE_SIMS = "DROP TABLE IF EXISTS " + SimContract.TABLE_NAME; - - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - db.execSQL(SQL_DELETE_SIMS); - onCreate(db); - } - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - onUpgrade(db, oldVersion, newVersion); - } -} diff --git a/app/src/main/java/com/hover/multisim/SimInfo.java b/app/src/main/java/com/hover/multisim/SimInfo.java deleted file mode 100644 index 562e16f..0000000 --- a/app/src/main/java/com/hover/multisim/SimInfo.java +++ /dev/null @@ -1,265 +0,0 @@ -package com.hover.multisim; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.Context; -import android.content.SharedPreferences; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; -import android.util.Log; - -import org.json.JSONArray; -import org.json.JSONException; - -import java.util.ArrayList; -import java.util.List; - -import io.sentry.Sentry; - -public class SimInfo { - private final static String TAG = "SimInfo"; - private final static String KEY = "sim_info_"; - - /** - * The slot that the SIM is in, starting from 0. Can be -1 if the SIM has been removed - */ - public int slotIdx = -1; - /** - * The Subscription ID assigned by Android. The same SIM can be assigned a new ID if it is removed and re-inserted. Hover will forget the old ID and update a SIM to the newest - */ - public int subscriptionId = -1; - - String imei; - - protected String iccId; - /** - * The Hardware identifier for the SIM. Hover uses this to track a SIM regardless of whether it is removed or its slot changed - */ - public String getIccId() { return iccId; } - - protected String imsi; - /** - * The The International Mobile Subscriber Identity used by the network to identify the SIM. The value reported here may be only the begining 5-6 digits or the whole thing. - * If you are trying to determine which network a SIM is for use this. The first 3 digits will always be the MCC and the following 2 or 3 will be the MNC which you can use to definitively identify which network this SIM is for - * See https://en.wikipedia.org/wiki/Mobile_country_code - */ - public String getImsi() { return imsi; } - String mcc; - String mnc; - - int simState = -1; - - protected String hni; - /** - * The Home Network Identifier. This is the first 5-6 digits of the IMSI, however, we recomend against using this since some devices may not report it correctly. Use the first 5-6 digits of the imsi using getImsi() - * - * @see SimInfo#getImsi() - */ - public String getOSReportedHni() { return hni; } - - protected String operatorName; - /** - * The name of the operator which provisioned the SIM. May differ from SIM to SIM distributed by the same network provisioner - */ - public String getOperatorName() { return operatorName; } - - protected String countryIso; - /** - * The country ISO of the operator which provisioned the SIM - */ - public String getCountryIso() { return countryIso; } - - String networkOperator; - /** - * The network of the operator which the SIM is connected to - */ - public String getNetworkOperator() { return networkOperator; } - String networkOperatorName; - /** - * The network name of the operator which the SIM is connected to - */ - public String getNetworkOperatorName() { return networkOperatorName; } - String networkCountryIso; - /** - * The country ISO of the operator which the SIM is connected to - */ - public String getNetworkCountryIso() { return networkCountryIso; } - int networkType; - // careful find networkTypeName because it will be different with networkType on same devices - - protected boolean networkRoaming = false; - /** - * Whether the SIM is currently roaming. Not guaranteed to be accurate. - */ - public boolean isRoaming() { return networkRoaming; } - - public SimInfo() {} - - public SimInfo(SlotManager slotMgr) { - if (slotMgr.slotIndex != null) slotIdx = slotMgr.slotIndex; - subscriptionId = slotMgr.subscriptionId; - - imei = slotMgr.imei; - simState = setSimState(slotMgr.findSimState()); - iccId = setStandardIccId(slotMgr.findIccId()); - imsi = slotMgr.findImsi(); - mcc = setMcc(imsi); - - hni = slotMgr.findOperator(); - operatorName = slotMgr.findOperatorName(); - countryIso = slotMgr.findCountryIso(); - networkOperator = slotMgr.findNetworkOperator(); - networkOperatorName = slotMgr.findNetworkOperatorName(); - networkCountryIso = slotMgr.findNetworkCountryIso(); - networkType = setNetworkType(slotMgr.findNetworkType()); - networkRoaming = setNetworkRoaming(slotMgr.findNetworkRoaming()); - -// Log.i(TAG, "Created SIM representation using reflection: " + this.log()); - } - - @TargetApi(22) - public SimInfo(SubscriptionInfo subInfo, Context c) { - subscriptionId = subInfo.getSubscriptionId(); - slotIdx = subInfo.getSimSlotIndex(); - - imsi = "" + subInfo.getMcc() + subInfo.getMnc(); - hni = "" + subInfo.getMcc() + subInfo.getMnc(); - mcc = "" + subInfo.getMcc(); - mnc = "" + subInfo.getMnc(); - iccId = setStandardIccId(subInfo.getIccId()); - - operatorName = (String) subInfo.getCarrierName(); // Is this Network Operator or Sim Operator? - countryIso = subInfo.getCountryIso(); - networkRoaming = SubscriptionManager.from(c).isNetworkRoaming(subscriptionId); - -// Can't load these until API 24 (Using TelephonyManager), but doesn't really matter: -// hnis, networkOperator, networkOperatorName, networkCountryIso, networkType -// These are not useful/inconsistent: -// Log.i(TAG, "SubInfo Display Name: " + si.getDisplayName()); -// Log.i(TAG, "SubInfo Number: " + si.getNumber()); - -// Log.i(TAG, "Created SIM representation using Subscription info: " + this.log()); - } - - public boolean isSameSim(SimInfo simInfo) { // FIXME: change so that if the SimInfo represents the same sim, the one with more/better info (and more accurate slotIdx?) is returned. It currently relies on order to do this, which is fragile - return simInfo != null && simInfo.iccId != null && iccId != null && iccId.equals(simInfo.iccId); - } - - private boolean isSameSimInSameSlot(SimInfo simInfo) { - return isSameSim(simInfo) && simInfo.slotIdx == slotIdx; - } - - public boolean isNotContainedIn(List simInfos) { - if (simInfos == null) return true; - for (SimInfo simInfo : simInfos) - if (this.isSameSim(simInfo)) - return false; - return true; - } - - public boolean isNotContainedInOrHasMoved(List simInfos) { - if (simInfos == null) return true; - for (SimInfo simInfo : simInfos) - if (this.isSameSimInSameSlot(simInfo)) - return false; - return true; - } - - public void save(Context c) { - new SimDataSource(c).saveToDb(this); - updateSubId(subscriptionId, c); - } - -// private void updateSlot(Context c) { -// Log.i(TAG, "Updating sim slot to: " + slotIdx); -// new SimDataSource(c).updateSlot(this, updatedInfo); -// updateSubId(updatedInfo.subscriptionId, c); -// } - public void setSimRemoved(Context c) { - Log.i(TAG, "Updating sim slot to: -1"); - new SimDataSource(c).remove(this); - } - - @SuppressLint("ApplySharedPref") - private void updateSubId(int subId, Context c) { - SharedPreferences.Editor editor = Utils.getSharedPrefs(c).edit(); - editor.putInt(KEY + SimContract.COLUMN_SUB_ID + iccId, subId); - editor.commit(); - } - public static int getSubId(String iccId, Context c) { - return Utils.getSharedPrefs(c).getInt(KEY + SimContract.COLUMN_SUB_ID + iccId, -1); - } - - public static List loadPresentByHni(JSONArray hniList, Context c) { - List simInfos = new ArrayList<>(); - for (int h = 0; h < hniList.length(); h++) { - try { - simInfos.addAll(new SimDataSource(c).getPresent(hniList.getString(h).substring(0, 3), hniList.getString(h).substring(3))); - } catch (JSONException | NullPointerException e) { Sentry.capture(e); } - } - return simInfos; - } - - public static SimInfo loadBySlot(int slotIdx, Context c) { - return new SimDataSource(c).get(slotIdx); - } - - public static List loadAll(Context c) { - return new SimDataSource(c).getAll(); - } - - String setStandardIccId(String iccId) { - if (iccId != null) - iccId = iccId.replaceAll("[a-zA-Z]", ""); - return iccId; - } - private String setMcc(String imsi) { return imsi != null ? imsi.substring(0, 3) : null; } - - boolean isMncMatch(int mncInt) { - return (imsi.length() == 4 && Integer.valueOf(imsi.substring(3)) == mncInt) || - (imsi.length() >= 5 && Integer.valueOf(imsi.substring(3, 5)) == mncInt) || - (imsi.length() >= 6 && Integer.valueOf(imsi.substring(3, 6)) == mncInt); - } - - public String getInterpretedHni(JSONArray actionHniList) { - for (int h = 0; h < actionHniList.length(); h++) { - int mncInt = Integer.valueOf(actionHniList.optString(h).substring(3)); - if (isMncMatch(mncInt)) { - return imsi.substring(0, 3) + mncInt; - } - } - return null; - } - - private int setSimState(Integer simState) { - if (simState == null) return -1; - return simState; - } - private int setNetworkType(Integer networkType) { - if (networkType == null) return 0; - return networkType; - } - private boolean setNetworkRoaming(Boolean networkRoaming) { return networkRoaming != null && networkRoaming; } - - - String log() { - return "slotIdx=[" + slotIdx + - "] subscriptionId=[" + subscriptionId + - "] imei=[" + imei + - "] imsi=[" + imsi + - "] simState=[" + simState + - "] simIccId=[" + iccId + - "] simHni=[" + hni + - "] simOperatorName=[" + operatorName + - "] simCountryIso=[" + countryIso + - "] networkOperator=[" + networkOperator + - "] networkOperatorName=[" + networkOperatorName + - "] networkCountryIso=[" + networkCountryIso + - "] networkType=[" + networkType + - "] networkRoaming=[" + networkRoaming + "]"; - } - - public String toString() { - return operatorName + " " + (countryIso != null ? countryIso.toUpperCase() : "") + " (SIM " + (slotIdx + 1) + ")"; - } -} diff --git a/app/src/main/java/com/hover/multisim/SlotManager.java b/app/src/main/java/com/hover/multisim/SlotManager.java deleted file mode 100644 index 7c0ee12..0000000 --- a/app/src/main/java/com/hover/multisim/SlotManager.java +++ /dev/null @@ -1,143 +0,0 @@ -package com.hover.multisim; - -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; - -import io.sentry.Sentry; - -import static android.telephony.TelephonyManager.SIM_STATE_READY; -import static android.telephony.TelephonyManager.SIM_STATE_UNKNOWN; - -final class SlotManager { - public final static String TAG = "SlotManager"; - final Integer slotIndex; - final Integer subscriptionId; - final String imei; - private final Object teleMgr; - private final Class teleClass; - - private final static String[] METHOD_SUFFIXES = new String[] { - "", - "Gemini", - "Ext", - "Ds", - "ForSubscription", - "ForPhone" - }; - - @SuppressWarnings("unused") - private SlotManager(int slotIdx, int subscriptionId, Object teleMgr, Class teleClass, Integer simState, String imei, String iccId) { -// Log.i(TAG, "Creating slotMgr. SlotIdx: " + slotIdx + " Mgr: " + teleMgr + " Class + " + teleClass + " IMEI: " + imei + " ICCID: " + iccId); - slotIndex = slotIdx; - this.subscriptionId = subscriptionId; - this.imei = imei; - this.teleMgr = teleMgr; - this.teleClass = teleClass; - } - - SimInfo createSimInfo() { - return new SimInfo(this); - } - - Object getValue(String methodName, Object subscriptionId) { - return getValue(methodName, (int) subscriptionId, teleMgr, teleClass); - } - - Integer findSimState() { return (Integer) getValue("getSimState", slotIndex); } - String findIccId() { return (String) getValue("getSimSerialNumber", subscriptionId); } - String findImsi() { return (String) getValue("getSubscriberId", subscriptionId); } - String findOperator() { return (String) getValue("getSimOperator", subscriptionId); } - String findOperatorName() { return (String) getValue("getSimOperatorName", subscriptionId); } - String findCountryIso() { return (String) getValue("getSimCountryIso", subscriptionId); } - String findNetworkOperator() { return (String) getValue("getNetworkOperator", subscriptionId); } - String findNetworkOperatorName() { return (String) getValue("getNetworkOperatorName", subscriptionId); } - String findNetworkCountryIso() { return (String) getValue("getNetworkCountryIso", subscriptionId); } - Integer findNetworkType() { return (Integer) getValue("getNetworkType", subscriptionId); } - boolean findNetworkRoaming() { return (boolean) getValue("isNetworkRoaming", subscriptionId); } - - private boolean isUnique(List slotMgrList) { - for (SlotManager mgr: slotMgrList) { - try { - if (mgr != null && imei.equals(mgr.imei) && teleMgr == mgr.teleMgr && teleClass == mgr.teleClass) - return false; - } catch (NullPointerException e) { Log.w(TAG, "something was null that shouldn't be", e); Sentry.capture(e); } - } -// Log.i(TAG, "Slot Manager was unique with Imei: " + imei + ", teleMgr: " + teleMgr + ", and teleClass: " + teleClass); - return true; - } - - public static void addValidReadySlots(List slotMgrList, Object slotIdx, Object teleMgrInstance, ArrayList validClassNames) { - if (slotIdx == null) - for (int i = 0; i < MultiSimWorker.SLOT_COUNT - 1; i++) - addValidReadySlots(slotMgrList, i, i, teleMgrInstance, validClassNames); - else - addValidReadySlots(slotMgrList, (int) slotIdx, (int) slotIdx, teleMgrInstance, validClassNames); - } - public static void addValidReadySlots(List slotMgrList, int slotIdx, int subscriptionId, Object teleMgrInstance, ArrayList validClassNames) { - if (validClassNames == null || validClassNames.size() <= 0) { return; } - for (String className : validClassNames) - if (teleMgrInstance != null || className != null) { - SlotManager sm = findValidReadySlot(slotIdx, subscriptionId, teleMgrInstance, className); - if (sm != null && (slotMgrList.size() == 0 || sm.isUnique(slotMgrList))) { - slotMgrList.add(sm); -// Log.e(TAG, "Added slotMgr. SlotIdx: " + slotIdx + " subId: " + subscriptionId + " Mgr: " + teleMgrInstance + " Class + " + sm.teleClass + " IMEI: " + sm.imei); // + " ICCID: " + getSimIccId(subscriptionId, teleMgr, sm.teleClass)); - } - } - } - private static SlotManager findValidReadySlot(int slotIdx, int subscriptionId, Object teleMgr, String className) { - Class teleClass = getTeleClass(teleMgr, className); - Integer simState = getSimState(slotIdx, teleMgr, teleClass); - String imei = getDeviceId(slotIdx, teleMgr, teleClass); - String iccId = getSimIccId(subscriptionId, teleMgr, teleClass); -// Log.i(TAG, "Got slot mgr with slotIdx: " + slotIdx + " subId: " + subscriptionId + " teleClass: " + teleClass + " simState: " + simState + " IMEI: " + imei + " ICCID: " + iccId + " IMSI: " + getSimImsi(subscriptionId, teleMgr, teleClass)); - if (simState != null && simState == SIM_STATE_READY && imei != null && iccId != null) - return new SlotManager(slotIdx, subscriptionId, teleMgr, teleClass, simState, imei, iccId); - return null; - } - - private static Integer getSimState(int slotIndex, Object teleMgr, Class teleClass) { - try { - return (Integer) getValue("getSimState", slotIndex, teleMgr, teleClass); - } catch (Exception e) { Log.d(TAG, "Couldn't get sim state"); return SIM_STATE_UNKNOWN; } - } - private static String getDeviceId(int slotIndex, Object teleMgr, Class teleClass) { - String imei = (String) getValue("getDeviceId", slotIndex, teleMgr, teleClass); - if (imei == null) imei = (String) getValue("getImei", slotIndex, teleMgr, teleClass); - return imei; - } - private static String getSimIccId(int subscriptionId, Object teleMgr, Class teleClass) { - return (String) getValue("getSimSerialNumber", subscriptionId, teleMgr, teleClass); - } - static String getSimImsi(int subscriptionId, Object teleMgr, Class teleClass) { - return (String) getValue("getSubscriberId", subscriptionId, teleMgr, teleClass); - } - - private static Class getTeleClass(Object teleMgr, String className) { - try { - if (className != null) return Class.forName(className); - } catch (ClassNotFoundException ignored) { } - if (teleMgr != null) return teleMgr.getClass(); - return null; - } - - private static Object getValue(String methodName, int slotIndex, Object teleMgr, Class teleClass) { - Object result = spamTeleMgr(methodName, teleMgr, teleClass, slotIndex); - if (result == null) result = spamTeleMgr(methodName, teleMgr, teleClass, null); - if (result == null) return null; - return result; - } - private static Object spamTeleMgr(String methodName, Object teleMgr, Class teleClass, Object subscriptionId) { - if (methodName == null || methodName.length() <= 0) return null; - Object result; - for (String methodSuffix : METHOD_SUFFIXES) { - result = MultiSimWorker.runMethodReflect(teleMgr, teleClass, methodName + methodSuffix, subscriptionId == null ? null : new Object[]{ subscriptionId }); - if (result != null) { -// Log.i(TAG, "Got result for method: " + methodName + methodSuffix + ". Result: " + result.toString()); - return result; - } - } - return null; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hover/multisim/Utils.java b/app/src/main/java/com/hover/multisim/Utils.java deleted file mode 100644 index 47d97a4..0000000 --- a/app/src/main/java/com/hover/multisim/Utils.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.hover.multisim; - -import android.Manifest; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.os.Build; - -import io.sentry.Sentry; - -public class Utils { - private static final String TAG = "Utils"; - private static final String SHARED_PREFS = "_multisim"; - - public static String getPackage(Context c) { - try { - return c.getApplicationContext().getPackageName(); - } catch (NullPointerException e) { - Sentry.capture(e); - return "fail"; - } - } - - public static boolean hasPhonePerm(Context c) { - return Build.VERSION.SDK_INT < 23 || (c.checkSelfPermission(Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED && - c.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED); - } - - public static SharedPreferences getSharedPrefs(Context context) { - return context.getSharedPreferences(getPackage(context) + SHARED_PREFS, Context.MODE_PRIVATE); - } -} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..a7950b8 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index a491964..f8c6127 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,10 @@ - #008577 - #00574B - #D81B60 - + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68e3975..7c9f826 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ - MultiSim - + Multisim + MainActivity + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml deleted file mode 100644 index 79a66e2..0000000 --- a/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..c00448c --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/test/java/com/hover/app/ExampleUnitTest.kt b/app/src/test/java/com/hover/app/ExampleUnitTest.kt new file mode 100644 index 0000000..fbc762c --- /dev/null +++ b/app/src/test/java/com/hover/app/ExampleUnitTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.app + +import junit.framework.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/app/src/test/java/com/hover/multisim/ExampleUnitTest.java b/app/src/test/java/com/hover/multisim/ExampleUnitTest.java deleted file mode 100644 index 93abdd5..0000000 --- a/app/src/test/java/com/hover/multisim/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.hover.multisim; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 48947c3..0000000 --- a/build.gradle +++ /dev/null @@ -1,27 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - repositories { - google() - jcenter() - - } - dependencies { - classpath 'com.android.tools.build:gradle:3.5.1' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - google() - jcenter() - - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..ccd596d --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,50 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +plugins { + id("com.android.application") version "7.3.0" apply false + id("com.android.library") version "7.3.0" apply false + id("org.jetbrains.kotlin.android") version "1.7.0" apply false + id("com.google.devtools.ksp") version "1.7.0-1.0.6" apply true + id("org.jlleitschuh.gradle.ktlint") version "10.2.0" + id("io.gitlab.arturbosch.detekt") version "1.18.0-RC2" + id("com.diffplug.spotless") version "6.0.0" + id("org.jetbrains.dokka") version "1.4.20" +} + +buildscript { + dependencies { + classpath("com.google.dagger:hilt-android-gradle-plugin:2.42") + } +} + +allprojects { + apply(plugin = "org.jetbrains.dokka") + apply(plugin = "org.jlleitschuh.gradle.ktlint") + ktlint { + android.set(true) + verbose.set(true) + filter { + exclude { element -> element.file.path.contains("generated/") } + } + } +} + +subprojects { + apply(plugin = "io.gitlab.arturbosch.detekt") + detekt { + config = files("${project.rootDir}/detekt.yml") + parallel = true + buildUponDefaultConfig = true + } + + apply(plugin = "com.diffplug.spotless") + spotless { + kotlin { + target("**/*.kt") + licenseHeaderFile( + rootProject.file("${project.rootDir}/spotless/copyright.kt"), + "^(package|object|import|interface)" + ) + } + } +} diff --git a/codeAnalysis.sh b/codeAnalysis.sh new file mode 100755 index 0000000..59e163c --- /dev/null +++ b/codeAnalysis.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +./gradlew ktlintFormat && ./gradlew ktlintCheck && ./gradlew detekt && ./gradlew spotlessApply + +# Before use it, in the first time, you must guarantee some running permissions: +# chmod +x codeAnalysis.sh +# +# After that, you just need to run: +# ./codeAnalysis.sh \ No newline at end of file diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 0000000..53c990a --- /dev/null +++ b/detekt.yml @@ -0,0 +1,538 @@ +build: + maxIssues: 0 + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +processors: + active: true + exclude: + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ClassCountProcessor' + # - 'PackageCountProcessor' + # - 'KtFileCountProcessor' + +console-reports: + active: true + exclude: + # - 'ProjectStatisticsReport' + # - 'ComplexityReport' + # - 'NotificationReport' + # - 'FindingsReport' + # - 'BuildFailureReport' + +comments: + active: true + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$) + UndocumentedPublicClass: + active: false + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + UndocumentedPublicFunction: + active: false + +complexity: + active: true + ComplexCondition: + active: false + threshold: 4 + ComplexInterface: + active: true + threshold: 10 + includeStaticDeclarations: false + ComplexMethod: + active: false + threshold: 10 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + LabeledExpression: + active: false + ignoredLabels: "" + LargeClass: + active: true + threshold: 600 + LongMethod: + active: false + threshold: 60 + LongParameterList: + active: true + threshold: 6 + ignoreDefaultParameters: false + MethodOverloading: + active: true + threshold: 6 + NestedBlockDepth: + active: true + threshold: 4 + StringLiteralDuplication: + active: true + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + thresholdInFiles: 30 + thresholdInClasses: 30 + thresholdInInterfaces: 30 + thresholdInObjects: 15 + thresholdInEnums: 15 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: false + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: 'toString,hashCode,equals,finalize' + InstanceOfCheckForException: + active: true + NotImplementedDeclaration: + active: false + PrintStackTrace: + active: false + RethrowCaughtException: + active: false + ReturnFromFinally: + active: true + SwallowedException: + active: false + ignoredExceptionTypes: 'InterruptedException,NumberFormatException,ParseException,MalformedURLException' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: true + ThrowingExceptionsWithoutMessageOrCause: + active: true + exceptions: 'IllegalArgumentException,IllegalStateException,IOException' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: false + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + exceptionNames: + - ArrayIndexOutOfBoundsException + - Error + - Exception + - IllegalMonitorStateException + - NullPointerException + - IndexOutOfBoundsException + - RuntimeException + - Throwable + allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + TooGenericExceptionThrown: + active: true + exceptionNames: + - Error + - Exception + - Throwable + - RuntimeException + +formatting: + active: true + android: true + ChainWrapping: + active: true + autoCorrect: true + CommentSpacing: + active: true + autoCorrect: true + Filename: + active: true + FinalNewline: + active: true + autoCorrect: true + ImportOrdering: + active: true + Indentation: + active: true + indentSize: 4 + continuationIndentSize: 4 + MaximumLineLength: + active: true + maxLineLength: 120 + ModifierOrdering: + active: true + autoCorrect: true + MultiLineIfElse: + active: true + autoCorrect: true + NoBlankLineBeforeRbrace: + active: true + autoCorrect: true + NoConsecutiveBlankLines: + active: true + autoCorrect: true + NoEmptyClassBody: + active: true + autoCorrect: true + NoLineBreakAfterElse: + active: true + autoCorrect: true + NoLineBreakBeforeAssignment: + active: true + autoCorrect: true + NoMultipleSpaces: + active: true + autoCorrect: true + NoSemicolons: + active: true + autoCorrect: true + NoTrailingSpaces: + active: true + autoCorrect: true + NoUnitReturn: + active: true + autoCorrect: true + NoUnusedImports: + active: true + autoCorrect: true + NoWildcardImports: + active: true + autoCorrect: true + PackageName: + active: true + autoCorrect: true + ParameterListWrapping: + active: true + autoCorrect: true + indentSize: 4 + SpacingAroundColon: + active: true + autoCorrect: true + SpacingAroundComma: + active: true + autoCorrect: true + SpacingAroundCurly: + active: true + autoCorrect: true + SpacingAroundDot: + active: true + autoCorrect: true + SpacingAroundKeyword: + active: true + autoCorrect: true + SpacingAroundOperators: + active: true + autoCorrect: true + SpacingAroundParens: + active: true + autoCorrect: true + SpacingAroundRangeOperator: + active: true + autoCorrect: true + StringTemplate: + active: true + autoCorrect: true + +naming: + active: true + ClassNaming: + active: true + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + classPattern: '[A-Z$][a-zA-Z0-9$]*' + ConstructorParameterNaming: + active: false + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + forbiddenName: '' + FunctionMaxLength: + active: false + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' + excludeClassPattern: '$^' + ignoreOverridden: true + FunctionParameterNaming: + active: true + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + InvalidPackageDeclaration: + active: false + rootPackage: '' + MatchingDeclarationName: + active: true + MemberNameEqualsClassName: + active: false + ignoreOverridden: true + ObjectPropertyNaming: + active: true + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$' + TopLevelPropertyNaming: + active: true + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + maximumVariableNameLength: 64 + VariableMinLength: + active: false + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + minimumVariableNameLength: 1 + VariableNaming: + active: true + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + +performance: + active: true + ArrayPrimitive: + active: true + ForEachOnRange: + active: true + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + SpreadOperator: + active: false + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + DuplicateCaseInWhenExpression: + active: true + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExplicitGarbageCollectionCall: + active: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + excludeAnnotatedProperties: "" + ignoreOnClassesPattern: "" + MissingWhenCase: + active: false + RedundantElseInWhen: + active: false + UnconditionalJumpStatementInLoop: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: false + UnsafeCast: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + CollapsibleIfStatements: + active: true + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: 'to' + DataClassShouldBeImmutable: + active: false + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: true + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: true + includeLineWrapping: false + ForbiddenComment: + active: true + values: 'FIXME:,STOPSHIP:' + ForbiddenImport: + active: false + imports: '' + ForbiddenVoid: + active: true + ignoreOverridden: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + excludedFunctions: 'describeContents' + LibraryCodeMustSpecifyReturnType: + active: true + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: false + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + ignoreNumbers: '-1,0,1,2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: true + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreLocalVariableDeclaration: true + MandatoryBracesIfStatements: + active: true + MaxLineLength: + active: false + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + MayBeConst: + active: true + ModifierOrder: + active: true + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: false + NoTabs: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: true + OptionalWhenBraces: + active: false + PreferToOverPairSyntax: + active: true + ProtectedMemberInFinalClass: + active: true + RedundantVisibilityModifierRule: + active: true + ReturnCount: + active: false + max: 2 + excludedFunctions: "equals" + excludeLabeled: false + excludeReturnFromLambda: true + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: true + ThrowsCount: + active: true + max: 5 + TrailingWhitespace: + active: true + UnderscoresInNumericLiterals: + active: false + acceptableDecimalLength: 5 + UnnecessaryAbstractClass: + active: false + excludeAnnotatedClasses: "dagger.Module" + UnnecessaryApply: + active: false + UnnecessaryInheritance: + active: true + UnnecessaryLet: + active: true + UnnecessaryParentheses: + active: false + UntilInsteadOfRangeTo: + active: true + UnusedImports: + active: true + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: false + allowedNames: '(_|ignored|expected|serialVersionUID)' + UseDataClass: + active: false + excludeAnnotatedClasses: "" + UseRequire: + active: false + UselessCallOnNotNull: + active: false + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + WildcardImport: + active: false + excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + excludeImports: 'java.util.*,kotlinx.android.synthetic.*' \ No newline at end of file diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..64b7e15 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,2 @@ +json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one +package_name("com.hover.app") # e.g. com.krausefx.app diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..c1d7d49 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,30 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:android) + +platform :android do + + desc "Runs static checks on the codebase" + lane :staticCheck do + gradle(task: "detekt") + end + + desc "Runs all unit tests" + lane :unitTest do + gradle(task: "test") + end + +end diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000..ac1849b --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,40 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## Android + +### android staticCheck + +```sh +[bundle exec] fastlane android staticCheck +``` + +Runs static checks on the codebase + +### android unitTest + +```sh +[bundle exec] fastlane android unitTest +``` + +Runs all unit tests + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/report.xml b/fastlane/report.xml new file mode 100644 index 0000000..7218776 --- /dev/null +++ b/fastlane/report.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..f276b12 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,41 @@ +[versions] +hilt = "2.42" +room = "2.4.2" +coroutines = "1.6.0" + +[libraries] +android-coreKtx = "androidx.core:core-ktx:1.9.0" + +android-material = "com.google.android.material:material:1.6.1" + +android-appcompat = "androidx.appcompat:appcompat:1.5.1" + +android-localbroadcastmanager = "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0" + +android-work = "androidx.work:work-runtime:2.7.1" + +android-hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +android-hilt-common = "androidx.hilt:hilt-common:1.0.0" +android-hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } +android-hilt-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } + +room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } + +sentry = "io.sentry:sentry-android:6.4.2" + +kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } + +test-junit4 = "junit:junit:4.13.2" +test-android-junit = "androidx.test.ext:junit:1.1.3" +test-robolectric = "org.robolectric:robolectric:4.8.1" +test-mockk = "io.mockk:mockk:1.12.7" +test-fixture = "com.appmattus.fixture:fixture:1.2.0" + +test-android-espresso = "androidx.test.espresso:espresso-core:3.4.0" + +[bundles] +hilt = ["android-hilt", "android-hilt-common", "android-hilt-compiler", "android-hilt-testing"] +room = ["room-ktx", "room-runtime"] \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index acb77f0..aa991fc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Nov 01 12:42:32 PDT 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/multisim/.gitignore b/multisim/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/multisim/.gitignore @@ -0,0 +1 @@ +/build diff --git a/multisim/build.gradle.kts b/multisim/build.gradle.kts new file mode 100644 index 0000000..9c02679 --- /dev/null +++ b/multisim/build.gradle.kts @@ -0,0 +1,206 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("dagger.hilt.android.plugin") + id("com.google.devtools.ksp") + id("maven-publish") + id("signing") + kotlin("kapt") +} + +android { + + namespace = "com.hover.multisim" + + compileSdk = 33 + + defaultConfig { + minSdk = 18 + targetSdk = 33 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + } + + kapt { + correctErrorTypes = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + dependencies { + implementation(libs.android.appcompat) + + implementation(libs.android.localbroadcastmanager) + + implementation(libs.android.work) + + implementation(libs.sentry) + + implementation(libs.bundles.hilt) + + implementation(libs.bundles.room) + ksp(libs.room.compiler) + + testImplementation(libs.test.junit4) + testImplementation(libs.kotlin.coroutines.test) + testImplementation(libs.test.robolectric) + testImplementation(libs.test.mockk) + testImplementation(libs.test.fixture) + } +} + +kotlin { + sourceSets { + all { + languageSettings.apply { + optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") + } + } + } +} + +tasks { + val sourceFiles = android.sourceSets.getByName("main").java.srcDirs + + register("withJavadoc") { + isFailOnError = false + dependsOn(android.libraryVariants.toList().last().javaCompileProvider) + + if (! project.plugins.hasPlugin("org.jetbrains.kotlin.android")) { + setSource(sourceFiles) + } + android.bootClasspath.forEach { classpath += project.fileTree(it) } + android.libraryVariants.forEach { variant -> + variant.javaCompileProvider.get().classpath.files.forEach { file -> + classpath += project.fileTree(file) + } + } + exclude("**/internal/*") + val options = options as StandardJavadocDocletOptions + options.links("https://developer.android.com/reference") + options.links("https://docs.oracle.com/javase/8/docs/api/") + + // Workaround for the following error when running on on JDK 9+ + // "The code being documented uses modules but the packages defined in ... are in the unnamed module." + if (JavaVersion.current() >= JavaVersion.VERSION_1_9) { + options.addStringOption("-release", "8") + } + } + + register("withJavadocJar") { + archiveClassifier.set("javadoc") + dependsOn(named("withJavadoc")) + val destination = named("withJavadoc").get().destinationDir + from(destination) + } + + register("withSourcesJar") { + archiveClassifier.set("sources") + from(sourceFiles) + } +} + +afterEvaluate { + fun Project.get(name: String, def: String = "$name not found") = + properties[name]?.toString() ?: System.getenv(name) ?: def + + fun Project.getRepositoryUrl(): java.net.URI { + val isReleaseBuild = !get("POM_VERSION_NAME").contains("SNAPSHOT") + val releaseRepoUrl = get( + "RELEASE_REPOSITORY_URL", + "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + ) + val snapshotRepoUrl = get( + "SNAPSHOT_REPOSITORY_URL", + "https://s01.oss.sonatype.org/content/repositories/snapshots/" + ) + return uri(if (isReleaseBuild) releaseRepoUrl else snapshotRepoUrl) + } + + publishing { + publications { + val props = project.properties + repositories { + maven { + url = getRepositoryUrl() + credentials { + username = project.get("ossUsername") + password = project.get("ossPassword") + } + } + } + val publicationName = props["POM_NAME"]?.toString() ?: "publication" + create(publicationName) { + from(project.components["release"]) + artifact(tasks.named("withJavadocJar")) + artifact(tasks.named("withSourcesJar")) + + pom { + groupId = project.get("GROUP") + artifactId = project.get("POM_ARTIFACT_ID") + version = project.get("VERSION_NAME") + + name.set(project.get("POM_NAME")) + description.set(project.get("POM_DESCRIPTION")) + url.set(project.get("POM_URL")) + packaging = project.get("POM_PACKAGING") + + scm { + url.set(project.get("POM_SCM_URL")) + connection.set(project.get("POM_SCM_CONNECTION")) + developerConnection.set(project.get("POM_SCM_DEV_CONNECTION")) + } + + organization { + name.set(project.get("POM_DEVELOPER_NAME")) + url.set(project.get("POM_DEVELOPER_URL")) + } + + developers { + developer { + id.set(project.get("POM_DEVELOPER_ID")) + name.set(project.get("POM_DEVELOPER_NAME")) + } + } + + licenses { + license { + name.set(project.get("POM_LICENCE_NAME")) + url.set(project.get("POM_LICENCE_URL")) + distribution.set(project.get("POM_LICENCE_DIST")) + } + } + } + } + + signing { + val signingKeyId = project.get("signingKeyId") + val signingKeyPassword = project.get("signingKeyPassword") + val signingKey = project.get("signingKey") + useInMemoryPgpKeys(signingKeyId, signingKey, signingKeyPassword) + sign(publishing.publications.getByName(publicationName)) + } + } + } +} diff --git a/multisim/gradle.properties b/multisim/gradle.properties new file mode 100644 index 0000000..388b97b --- /dev/null +++ b/multisim/gradle.properties @@ -0,0 +1,21 @@ +GROUP= +POM_ARTIFACT_ID= +VERSION_NAME=0.0.1 +POM_PACKAGING=aar + +POM_NAME= +POM_DESCRIPTION= +POM_INCEPTION_YEAR= +POM_URL=https://github.com/UseHover/MultiSim + +POM_LICENSE_NAME= +POM_LICENSE_URL= +POM_LICENSE_DIST= + +POM_SCM_URL= +POM_SCM_CONNECTION= +POM_SCM_DEV_CONNECTION= + +POM_DEVELOPER_ID= +POM_DEVELOPER_NAME= +POM_DEVELOPER_URL=https://github.com/UseHover \ No newline at end of file diff --git a/multisim/schemas/com.hover.multisim.data.database.Database/1.json b/multisim/schemas/com.hover.multisim.data.database.Database/1.json new file mode 100644 index 0000000..e7ab190 --- /dev/null +++ b/multisim/schemas/com.hover.multisim.data.database.Database/1.json @@ -0,0 +1,140 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "64772d7bc9ec08680b3c8ce4ced8e2e0", + "entities": [ + { + "tableName": "hsdk_sims", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `slot_idx` INTEGER NOT NULL, `sub_id` INTEGER NOT NULL, `imei` TEXT, `state` INTEGER NOT NULL, `imsi` TEXT NOT NULL, `mcc` TEXT NOT NULL, `mnc` TEXT, `iccid` TEXT NOT NULL, `operator` TEXT, `operator_name` TEXT, `country_iso` TEXT, `is_roaming` INTEGER NOT NULL, `network_code` TEXT, `network_name` TEXT, `network_country` TEXT, `network_type` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "slot_idx", + "columnName": "slot_idx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sub_id", + "columnName": "sub_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imei", + "columnName": "imei", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imsi", + "columnName": "imsi", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mcc", + "columnName": "mcc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mnc", + "columnName": "mnc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iccid", + "columnName": "iccid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "operator", + "columnName": "operator", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "operator_name", + "columnName": "operator_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "country_iso", + "columnName": "country_iso", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "is_roaming", + "columnName": "is_roaming", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "network_code", + "columnName": "network_code", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "network_name", + "columnName": "network_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "network_country", + "columnName": "network_country", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "network_type", + "columnName": "network_type", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_hsdk_sims_iccid", + "unique": true, + "columnNames": [ + "iccid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_hsdk_sims_iccid` ON `${TABLE_NAME}` (`iccid`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '64772d7bc9ec08680b3c8ce4ced8e2e0')" + ] + } +} \ No newline at end of file diff --git a/multisim/schemas/com.hover.multisim.db.Database/1.json b/multisim/schemas/com.hover.multisim.db.Database/1.json new file mode 100644 index 0000000..e7ab190 --- /dev/null +++ b/multisim/schemas/com.hover.multisim.db.Database/1.json @@ -0,0 +1,140 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "64772d7bc9ec08680b3c8ce4ced8e2e0", + "entities": [ + { + "tableName": "hsdk_sims", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `slot_idx` INTEGER NOT NULL, `sub_id` INTEGER NOT NULL, `imei` TEXT, `state` INTEGER NOT NULL, `imsi` TEXT NOT NULL, `mcc` TEXT NOT NULL, `mnc` TEXT, `iccid` TEXT NOT NULL, `operator` TEXT, `operator_name` TEXT, `country_iso` TEXT, `is_roaming` INTEGER NOT NULL, `network_code` TEXT, `network_name` TEXT, `network_country` TEXT, `network_type` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "slot_idx", + "columnName": "slot_idx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sub_id", + "columnName": "sub_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imei", + "columnName": "imei", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imsi", + "columnName": "imsi", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mcc", + "columnName": "mcc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mnc", + "columnName": "mnc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iccid", + "columnName": "iccid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "operator", + "columnName": "operator", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "operator_name", + "columnName": "operator_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "country_iso", + "columnName": "country_iso", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "is_roaming", + "columnName": "is_roaming", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "network_code", + "columnName": "network_code", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "network_name", + "columnName": "network_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "network_country", + "columnName": "network_country", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "network_type", + "columnName": "network_type", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_hsdk_sims_iccid", + "unique": true, + "columnNames": [ + "iccid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_hsdk_sims_iccid` ON `${TABLE_NAME}` (`iccid`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '64772d7bc9ec08680b3c8ce4ced8e2e0')" + ] + } +} \ No newline at end of file diff --git a/multisim/src/main/AndroidManifest.xml b/multisim/src/main/AndroidManifest.xml new file mode 100644 index 0000000..47d3baf --- /dev/null +++ b/multisim/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/multisim/src/main/java/com/hover/multisim/data/database/Database.kt b/multisim/src/main/java/com/hover/multisim/data/database/Database.kt new file mode 100644 index 0000000..e8ef828 --- /dev/null +++ b/multisim/src/main/java/com/hover/multisim/data/database/Database.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.multisim.data.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.hover.multisim.data.database.dao.SimDao +import com.hover.multisim.data.database.model.HSDKSims + +@Database( + entities = [ + HSDKSims::class + ], + version = 1, + exportSchema = true +) + +abstract class Database : RoomDatabase() { + abstract fun simDao(): SimDao +} diff --git a/multisim/src/main/java/com/hover/multisim/data/database/dao/BaseDao.kt b/multisim/src/main/java/com/hover/multisim/data/database/dao/BaseDao.kt new file mode 100644 index 0000000..f38bc30 --- /dev/null +++ b/multisim/src/main/java/com/hover/multisim/data/database/dao/BaseDao.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.multisim.data.database.dao + +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy.REPLACE +import androidx.room.Update + +/** + * BaseDao + * + * This dao interface makes it easy to abstract commonly used room operations + * + * @param T takes in the data class + */ +interface BaseDao { + + @Insert(onConflict = REPLACE) + suspend fun insert(item: T): Long + + @Insert(onConflict = REPLACE) + suspend fun insert(items: List) + + @Update(onConflict = REPLACE) + suspend fun update(item: T): Int + + @Update(onConflict = REPLACE) + suspend fun update(items: List): Int + + @Delete + suspend fun delete(item: T) +} diff --git a/multisim/src/main/java/com/hover/multisim/data/database/dao/SimDao.kt b/multisim/src/main/java/com/hover/multisim/data/database/dao/SimDao.kt new file mode 100644 index 0000000..129cc45 --- /dev/null +++ b/multisim/src/main/java/com/hover/multisim/data/database/dao/SimDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.multisim.data.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.hover.multisim.data.database.model.HSDKSims +import kotlinx.coroutines.flow.Flow + +@Dao +interface SimDao : BaseDao { + + @Query("SELECT * FROM hsdk_sims") + fun getAllSims(): Flow> // return SimInfo + + @Query("SELECT * FROM hsdk_sims WHERE mcc =:mcc AND slot_idx != -1") + fun getPresentSims(mcc: String): Flow> // return SimInfo + + @Query("SELECT * FROM hsdk_sims WHERE slot_idx =:slotIdx LIMIT 1") + suspend fun getSim(slotIdx: Int): HSDKSims // return SimInfo + + @Query("SELECT * FROM hsdk_sims WHERE iccId =:iccId LIMIT 1") + suspend fun loadBySim(iccId: String): HSDKSims // return SimInfo +} diff --git a/multisim/src/main/java/com/hover/multisim/data/database/model/HSDKSims.kt b/multisim/src/main/java/com/hover/multisim/data/database/model/HSDKSims.kt new file mode 100644 index 0000000..83ce9ed --- /dev/null +++ b/multisim/src/main/java/com/hover/multisim/data/database/model/HSDKSims.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.multisim.data.database.model + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "hsdk_sims", indices = [Index(value = ["iccid"], unique = true)] +) +data class HSDKSims( + @PrimaryKey(autoGenerate = true) val id: Long, + val slot_idx: Int, + val sub_id: Int, + val imei: String?, + val state: Int, + val imsi: String, + val mcc: String, + val mnc: String?, + val iccid: String, + val operator: String?, + val operator_name: String?, + val country_iso: String?, + val is_roaming: Boolean, + val network_code: String?, + val network_name: String?, + val network_country: String?, + val network_type: Int?, +) diff --git a/multisim/src/main/java/com/hover/multisim/data/database/util/mapper.kt b/multisim/src/main/java/com/hover/multisim/data/database/util/mapper.kt new file mode 100644 index 0000000..b5d8e0b --- /dev/null +++ b/multisim/src/main/java/com/hover/multisim/data/database/util/mapper.kt @@ -0,0 +1,42 @@ +package com.hover.multisim.sim + +import com.hover.multisim.data.database.model.HSDKSims + +fun HSDKSims.toSimInfo() = SimInfo( + slotIdx = this.slot_idx, + subscriptionId = this.sub_id, + imei = this.imei, + simState = this.state, + imsi = this.imsi, + mcc = this.mcc, + mnc = this.mnc, + iccId = this.iccid, + hni = this.operator, + operatorName = this.operator_name, + countryIso = this.country_iso, + networkRoaming = this.is_roaming, + networkOperator = this.network_code, + networkOperatorName = this.network_name, + networkCountryIso = this.network_country, + networkType = this.network_type +) + +fun SimInfo.toSimInfo() = HSDKSims( + id = 0, + slot_idx = this.slotIdx, + sub_id = this.subscriptionId, + imei = this.imei, + state = this.simState, + imsi = this.imsi, + mcc = this.mcc, + mnc = this.mnc, + iccid = this.iccId, + operator = this.hni, + operator_name = this.operatorName, + country_iso = this.countryIso, + is_roaming = this.networkRoaming, + network_code = this.networkOperator, + network_name = this.networkOperatorName, + network_country = this.networkCountryIso, + network_type = this.networkType +) diff --git a/multisim/src/main/java/com/hover/multisim/data/repository/SimRepository.kt b/multisim/src/main/java/com/hover/multisim/data/repository/SimRepository.kt new file mode 100644 index 0000000..81b7326 --- /dev/null +++ b/multisim/src/main/java/com/hover/multisim/data/repository/SimRepository.kt @@ -0,0 +1,30 @@ +package com.hover.multisim.data.repository + +import com.hover.multisim.data.database.dao.SimDao +import com.hover.multisim.sim.SimInfo +import com.hover.multisim.sim.toSimInfo +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +interface SimRepository { + fun getAllSims(): Flow> + fun getPresentSims(mcc: String): Flow> + suspend fun getSim(slotIdx: Int): SimInfo + suspend fun loadBySim(iccId: String): SimInfo +} + +class SimRepositoryImpl @Inject constructor( + private val simDao: SimDao +) : SimRepository { + + override fun getAllSims(): Flow> = + simDao.getAllSims().map { hsdkSimsList -> hsdkSimsList.map { it.toSimInfo() } } + + override fun getPresentSims(mcc: String): Flow> = + simDao.getPresentSims(mcc).map { hsdkSimsList -> hsdkSimsList.map { it.toSimInfo() } } + + override suspend fun getSim(slotIdx: Int): SimInfo = simDao.getSim(slotIdx).toSimInfo() + + override suspend fun loadBySim(iccId: String): SimInfo = simDao.loadBySim(iccId).toSimInfo() +} diff --git a/multisim/src/main/java/com/hover/multisim/di/DaoModule.kt b/multisim/src/main/java/com/hover/multisim/di/DaoModule.kt new file mode 100644 index 0000000..c672d03 --- /dev/null +++ b/multisim/src/main/java/com/hover/multisim/di/DaoModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.multisim.di + +import com.hover.multisim.data.database.Database +import com.hover.multisim.data.database.dao.SimDao +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object DaoModule { + + @Provides + fun providesSimDao( + database: Database, + ): SimDao = database.simDao() +} diff --git a/multisim/src/main/java/com/hover/multisim/di/DatabaseModule.kt b/multisim/src/main/java/com/hover/multisim/di/DatabaseModule.kt new file mode 100644 index 0000000..cf11068 --- /dev/null +++ b/multisim/src/main/java/com/hover/multisim/di/DatabaseModule.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.multisim.di + +import android.content.Context +import androidx.room.Room +import com.hover.multisim.data.database.Database +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + @Provides + @Singleton + fun providesDatabase( + @ApplicationContext context: Context, + ): Database = Room.databaseBuilder( + context, + Database::class.java, + "multisim" + ).build() +} diff --git a/multisim/src/main/java/com/hover/multisim/sim/MultiSimWorker.kt b/multisim/src/main/java/com/hover/multisim/sim/MultiSimWorker.kt new file mode 100644 index 0000000..346317a --- /dev/null +++ b/multisim/src/main/java/com/hover/multisim/sim/MultiSimWorker.kt @@ -0,0 +1,401 @@ +package com.hover.multisim.sim + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.SystemClock +import android.telephony.PhoneStateListener +import android.telephony.ServiceState +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.util.Log +import androidx.hilt.work.HiltWorker +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.work.ListenableWorker +import androidx.work.OneTimeWorkRequest +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkerParameters +import androidx.work.impl.utils.futures.SettableFuture +import com.google.common.util.concurrent.ListenableFuture +import dagger.assisted.Assisted +import io.sentry.Sentry +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit + +// TODO - Maybe rewrite this implementation somehow to simplify it? +@HiltWorker +class MultiSimWorker( + @Assisted context: Context, + @Assisted params: WorkerParameters, +) : ListenableWorker(context, params) { + + companion object { + const val SLOT_COUNT = 3 // Need to check 0, 1, and 2. Some phones index from 1. + } + + private val NEW_SIM_INFO = "NEW_SIM_INFO_ACTION" + + private lateinit var workerFuture: SettableFuture + private var result: Result? = null + + private val slotSemaphore = Semaphore(1, true) + private val simSemaphore = Semaphore(1, true) + + private var simStateReceiver: SimStateReceiver? = null + private var simStateListener: SimStateListener? = null + private var validClassNames: ArrayList? = null + + private val POSS_CLASS_NAMES = arrayOf( + null, + "android.telephony.TelephonyManager", + "android.telephony.MSimTelephonyManager", + "android.telephony.MultiSimTelephonyService", + "com.mediatek.telephony.TelephonyManagerEx", + "com.android.internal.telephony.Phone", + "com.android.internal.telephony.PhoneFactory" + ) + + fun makeToil(): PeriodicWorkRequest { + return PeriodicWorkRequest.Builder(MultiSimWorker::class.java, 15, TimeUnit.MINUTES).build() + } + + fun makeWork(): OneTimeWorkRequest { + return OneTimeWorkRequest.Builder(MultiSimWorker::class.java).build() + } + + @SuppressLint("RestrictedApi") + override fun startWork(): ListenableFuture { + workerFuture = SettableFuture.create() + if (Utils.hasPhonePerm(applicationContext)) { + startListeners() + } else { + workerFuture.set(Result.failure()) + } + return workerFuture + } + + /* ktlint-disable max-line-length */ + @SuppressLint("RestrictedApi") + private fun startListeners() { + try { + registerSimStateReceiver() + if (simStateListener == null) simStateListener = SimStateListener() + // TelephonyManager.listen() must take place on the main thread + (applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager).listen( + simStateListener, + PhoneStateListener.LISTEN_SERVICE_STATE or PhoneStateListener.LISTEN_SIGNAL_STRENGTHS + ) + } catch (e: java.lang.Exception) { + workerFuture.set(Result.retry()) + } + } + + @SuppressLint("RestrictedApi") + @Synchronized + private fun updateSimInfo() { + backgroundExecutor.execute { + result = try { + simSemaphore.acquire() + if (Utils.hasPhonePerm(applicationContext)) { + val oldList: List? = getSaved() + val newList = findUniqueSimInfo() + run { + compareNewAndOld(newList, oldList) + Result.success() + } + } else Result.failure() + } catch (e: java.lang.Exception) { + Sentry.captureException(e) + Result.failure() + } finally { + simSemaphore.release() + // Give the listeners a chance to receive a few events - sometimes the first trigger isn't the needed info. 5s is long but this is a background thread anyway and the info has already been updated in the DB + SystemClock.sleep(5000) + if (!workerFuture.isDone) { + workerFuture.set(result) + } + } + } + } + + private fun compareNewAndOld(newList: List, oldList: List?) { + if (oldList == null || oldList.size != newList.size) { + onSimInfoUpdate(newList) + } else { + for (i in newList.indices) if (newList[i].isNotContainedInOrHasMoved(oldList)) { + onSimInfoUpdate(newList) + break + } + } + } + + private fun getSaved(): java.util.ArrayList? { + var oldList: java.util.ArrayList? = null + for (i in 0 until SLOT_COUNT) { + val si: SimInfo = SimDataSource(applicationContext).get(i) + if (oldList == null) oldList = java.util.ArrayList() + oldList.add(si) + } + return oldList + } + + private fun onSimInfoUpdate(newList: List) { + updateDb(newList) + LocalBroadcastManager.getInstance(applicationContext).sendBroadcast( + Intent( + MultiSimWorker.action( + applicationContext + ) + ) + ) + } + + private fun updateDb(newList: List) { + for (si in newList) { + si.save(applicationContext) + } + val dbInfos: List = SimDataSource(applicationContext).getAll() + for (dbSi in dbInfos) { + if (!findPhysicalSim(newList, dbSi)) { + dbSi.setSimRemoved(applicationContext) + } + } + } + + private fun findPhysicalSim(newList: List, savedSim: SimInfo): Boolean { + for (si in newList) { + if (si.isSameSim(savedSim)) return true + } + return false + } + + @Synchronized + @SuppressWarnings("MissingPermission") + private fun findUniqueSimInfo(): List { + var newList: List = java.util.ArrayList() + try { + slotSemaphore.acquire() + val slotMgrList: List = java.util.ArrayList() + val teleMgrInstances = listTeleMgrs(slotMgrList) + var subInfos: List? = java.util.ArrayList() + if (Build.VERSION.SDK_INT >= 22) subInfos = + getSubscriptions(teleMgrInstances, slotMgrList) + newList = if (subInfos != null && subInfos.isNotEmpty()) createUniqueSimInfoList( + subInfos, slotMgrList + ) else createUniqueSimInfoList(slotMgrList) + } catch (e: java.lang.Exception) { + Sentry.captureException(e) + } finally { + slotSemaphore.release() + } + return newList + } + + private fun createUniqueSimInfoList( + subInfos: List, slotMgrList: List + ): List { + val newList = createUniqueSimInfoList(slotMgrList) + for (subInfo in subInfos) newList.add(SimInfo(subInfo, applicationContext)) + return removeDuplicates(newList) + } + + private fun createUniqueSimInfoList(slotMgrList: List): MutableList { + val newList: MutableList = java.util.ArrayList() + for (sm in slotMgrList) newList.add(sm.createSimInfo()) + return removeDuplicates(newList) + } + + private fun removeDuplicates(simInfos: List): MutableList { + val uniqueSimInfos: MutableList = java.util.ArrayList() + for (simInfo in simInfos) if (simInfo.isNotContainedIn(uniqueSimInfos)) uniqueSimInfos.add( + simInfo + ) + return uniqueSimInfos + } + + @TargetApi(22) + @SuppressWarnings("MissingPermission") + private fun getSubscriptions( + teleMgrInstances: List?, slotMgrList: List + ): List? { + val subInfos = SubscriptionManager.from( + applicationContext + ).activeSubscriptionInfoList + if (teleMgrInstances != null) { + for (teleMgr in teleMgrInstances) { + if (subInfos != null) { + for (subinfo in subInfos) SlotManager.addValidReadySlots( + slotMgrList, + subinfo.simSlotIndex, + subinfo.subscriptionId, + teleMgr, + validClassNames + ) + } + } + } + return subInfos + } + + private fun listTeleMgrs(slotMgrList: List): List { + if (validClassNames == null || validClassNames!!.isEmpty()) validClassNames = + java.util.ArrayList( + listOf(*POSS_CLASS_NAMES) + ) + val teleMgrList: MutableList = java.util.ArrayList() + + for (className in POSS_CLASS_NAMES) { + if (className == null) continue + addMgrFromReflection(className, teleMgrList, null, slotMgrList) + for (i in 0 until SLOT_COUNT) addMgrFromReflection( + className, teleMgrList, i, slotMgrList + ) + } + addMgrFromSystemService(Context.TELEPHONY_SERVICE, teleMgrList, null, slotMgrList) + addMgrFromSystemService("phone_msim", teleMgrList, null, slotMgrList) + for (j in 0 until SLOT_COUNT) addMgrFromSystemService( + "phone$j", teleMgrList, j, slotMgrList + ) + teleMgrList.add(null) + return teleMgrList + } + + private fun addMgrFromReflection( + className: String, + teleMgrList: MutableList, + slotIdx: Any?, + slotMgrList: List + ) { + val result = runMethodReflect(className, "getDefault", slotIdx?.let { arrayOf(it) }) + if (result != null && !teleMgrList.contains(result)) { + teleMgrList.add(result) + if (Build.VERSION.SDK_INT < 22) SlotManager.addValidReadySlots( + slotMgrList, slotIdx, result, validClassNames + ) + } + } + + private fun addMgrFromSystemService( + serviceName: String, + teleMgrList: MutableList, + slotIdx: Any, + slotMgrList: List + ) { + val serv = applicationContext.getSystemService(serviceName) + if (serv != null && !teleMgrList.contains(serv)) { + teleMgrList.add(serv) + if (Build.VERSION.SDK_INT < 22) SlotManager.addValidReadySlots( + slotMgrList, slotIdx, serv, validClassNames + ) + } + } + + private fun runMethodReflect( + actualInstance: Any, methodName: String, methodParams: Array + ): Any? { + return runMethodReflect(actualInstance, actualInstance.javaClass, methodName, methodParams) + } + + private fun runMethodReflect( + actualInstance: Any?, classInstance: Class<*>, methodName: String, methodParams: Array + ): Any? { + var result: Any? = null + try { + val method = classInstance.getDeclaredMethod( + methodName, *getClassParams(methodParams) + ) + val accessible = method.isAccessible + method.isAccessible = true + result = method.invoke(actualInstance ?: classInstance, *methodParams) + method.isAccessible = accessible + } catch (ignored: java.lang.Exception) { + } + return result + } + + fun runMethodReflect( + className: String, methodName: String, methodParams: Array + ): Any? { + try { + return runMethodReflect(null, Class.forName(className), methodName, methodParams) + } catch (e: ClassNotFoundException) { + validClassNames!!.remove(className) + } + return null + } + + private fun runFieldReflect(className: String, field: String): Any? { + var result: Any? = null + try { + val classInstance = Class.forName(className) + val fieldReflect = classInstance.getField(field) + val accessible = fieldReflect.isAccessible + fieldReflect.isAccessible = true + result = fieldReflect[null]?.toString() + fieldReflect.isAccessible = accessible + } catch (ignored: java.lang.Exception) { + } + return result + } + + private fun getClassParams(methodParams: Array?): Array?>? { + var classesParams: Array?>? = null + if (methodParams != null) { + classesParams = arrayOfNulls?>(methodParams.size) + for (i in methodParams.indices) { + if (methodParams[i] is Int) classesParams[i] = + Int::class.javaPrimitiveType // logString += methodParams[i] + ","; + else if (methodParams[i] is String) classesParams[i] = + String::class.java // logString += "\"" + methodParams[i] + "\","; + else if (methodParams[i] is Long) classesParams[i] = + Long::class.javaPrimitiveType // logString += methodParams[i] + ","; + else if (methodParams[i] is Boolean) classesParams[i] = + Boolean::class.javaPrimitiveType // logString += methodParams[i] + ","; + else classesParams[i] = + methodParams[i].javaClass // logString += "["+methodParams[i]+"]" + ","; + } + } + return classesParams + } + + private fun registerSimStateReceiver() { + if (simStateReceiver == null) { + simStateReceiver = SimStateReceiver() + val intentFilter = IntentFilter() + intentFilter.addAction("android.intent.action.SIM_STATE_CHANGED") + intentFilter.addAction("android.intent.action.ACTION_SIM_STATE_CHANGED") + intentFilter.addAction("android.intent.action.PHONE_STATE") + intentFilter.addAction("vivo.intent.action.ACTION_SIM_STATE_CHANGED") + applicationContext.registerReceiver(simStateReceiver, intentFilter) + } + } + + private class SimStateListener : PhoneStateListener() { + override fun onServiceStateChanged(serviceState: ServiceState) { + updateSimInfo() + } + } + + private class SimStateReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + updateSimInfo() + } + } + + override fun onStopped() { + super.onStopped() + try { + (applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager).listen( + simStateListener, PhoneStateListener.LISTEN_NONE + ) + simStateListener = null + if (simStateReceiver != null) applicationContext.unregisterReceiver(simStateReceiver) + simStateReceiver = null + } catch (ignored: Exception) { } + } +} diff --git a/multisim/src/main/java/com/hover/multisim/sim/SimInfo.kt b/multisim/src/main/java/com/hover/multisim/sim/SimInfo.kt new file mode 100644 index 0000000..8090d9e --- /dev/null +++ b/multisim/src/main/java/com/hover/multisim/sim/SimInfo.kt @@ -0,0 +1,45 @@ +package com.hover.multisim.sim + +data class SimInfo( + val slotIdx: Int, + val subscriptionId: Int, + val imei: String?, + val simState: Int, + val imsi: String, + val mcc: String, + val mnc: String?, + val iccId: String, + val hni: String?, + val operatorName: String?, + val countryIso: String?, + val networkRoaming: Boolean, + val networkOperator: String?, + val networkOperatorName: String?, + val networkCountryIso: String?, + val networkType: Int? +) { + + fun isMncMatch(mncInt: Int): Boolean { + return imsi.length == 4 && imsi.substring(3) + .toInt() == mncInt || imsi.length >= 5 && imsi.substring(3, 5) + .toInt() == mncInt || imsi.length >= 6 && imsi.substring(3, 6).toInt() == mncInt + } + + fun isNotContainedInOrHasMoved(simInfos: List?): Boolean { + if (simInfos == null) return true + for (simInfo in simInfos) if (simInfo.let { this.isSameSimInSameSlot(it) }) return false + return true + } + + private fun isSameSimInSameSlot(simInfo: SimInfo): Boolean = + isSameSim(simInfo) && simInfo.slotIdx == slotIdx + + // FIXME: change so that if the SimInfo represents the same sim, the one with more/better info (and more accurate slotIdx?) is returned. It currently relies on order to do this, which is fragile + fun isSameSim(simInfo: SimInfo?): Boolean = simInfo?.iccId != null && iccId == simInfo.iccId + + fun isNotContainedIn(simInfos: List?): Boolean { + if (simInfos == null) return true + for (simInfo in simInfos) if (isSameSim(simInfo)) return false + return true + } +} diff --git a/multisim/src/main/java/com/hover/multisim/sim/SlotManager.kt b/multisim/src/main/java/com/hover/multisim/sim/SlotManager.kt new file mode 100644 index 0000000..a409c1d --- /dev/null +++ b/multisim/src/main/java/com/hover/multisim/sim/SlotManager.kt @@ -0,0 +1,167 @@ +package com.hover.multisim.sim + +import android.telephony.TelephonyManager +import io.sentry.Sentry + +class SlotManager( + slotIdx: Int, + subscriptionId: Int, + private var teleMgr: Any?, + private var teleClass: Class<*>?, + simState: Int, + private var imei: String?, + iccId: String? +) { + + private var slotIndex: Int? = slotIdx + private var subscriptionId: Int? = subscriptionId + + private val METHOD_SUFFIXES = arrayOf( + "", "Gemini", "Ext", "Ds", "ForSubscription", "ForPhone" + ) + + operator fun getValue(methodName: String, subscriptionId: Any): Any? = + getValue(methodName, subscriptionId as Int, teleMgr, teleClass) + + fun findSimState(): Int? = slotIndex?.let { getValue("getSimState", it) } as Int? + + fun findIccId(): String? = subscriptionId?.let { getValue("getSimSerialNumber", it) } as String? + + fun findImsi(): String? = subscriptionId?.let { getValue("getSubscriberId", it) } as String? + + fun findOperator(): String? = subscriptionId?.let { getValue("getSimOperator", it) } as String? + + fun findOperatorName(): String? = + subscriptionId?.let { getValue("getSimOperatorName", it) } as String? + + fun findCountryIso(): String? = + subscriptionId?.let { getValue("getSimCountryIso", it) } as String? + + fun findNetworkOperator(): String? = + subscriptionId?.let { getValue("getNetworkOperator", it) } as String? + + fun findNetworkOperatorName(): String? = + subscriptionId?.let { getValue("getNetworkOperatorName", it) } as String? + + fun findNetworkCountryIso(): String? = + subscriptionId?.let { getValue("getNetworkCountryIso", it) } as String? + + fun findNetworkType(): Int? = subscriptionId?.let { getValue("getNetworkType", it) } as Int? + + fun findNetworkRoaming(): Boolean = + subscriptionId?.let { getValue("isNetworkRoaming", it) } as Boolean + + /* ktlint-disable max-line-length */ + private fun isUnique(slotMgrList: List): Boolean { + for (mgr in slotMgrList) { + try { + if (imei == mgr.imei && teleMgr === mgr.teleMgr && teleClass == mgr.teleClass) return false + } catch (e: NullPointerException) { + Sentry.captureException(e) + } + } + return true + } + + fun addValidReadySlots( + slotMgrList: List, + slotIdx: Any?, + teleMgrInstance: Any?, + validClassNames: ArrayList? + ) { + if (slotIdx == null) for (i in 0 until MultiSimWorker.SLOT_COUNT - 1) addValidReadySlots( + slotMgrList, i, i, teleMgrInstance, validClassNames + ) else addValidReadySlots( + slotMgrList, slotIdx as Int, slotIdx, teleMgrInstance, validClassNames + ) + } + + /* ktlint-disable max-line-length */ + private fun addValidReadySlots( + slotMgrList: List, + slotIdx: Int, + subscriptionId: Int, + teleMgrInstance: Any?, + validClassNames: java.util.ArrayList? + ) { + if (validClassNames == null || validClassNames.size <= 0) { + return + } + for (className in validClassNames) if (teleMgrInstance != null || className != null) { + val sm: SlotManager? = + findValidReadySlot(slotIdx, subscriptionId, teleMgrInstance, className) + if (sm != null) { + if (slotMgrList.isEmpty() || sm.isUnique(slotMgrList)) { + slotMgrList + sm + } + } + } + } + + private fun findValidReadySlot( + slotIdx: Int, subscriptionId: Int, teleMgr: Any?, className: String? + ): SlotManager? { + val teleClass: Class<*>? = getTeleClass(teleMgr, className) + val simState: Int? = getSimState(slotIdx, teleMgr, teleClass) + val imei: String? = getDeviceId(slotIdx, teleMgr, teleClass) + val iccId: String? = getSimIccId(subscriptionId, teleMgr, teleClass) + return if (simState == TelephonyManager.SIM_STATE_READY) SlotManager( + slotIdx, subscriptionId, teleMgr, teleClass, simState, imei, iccId + ) else null + } + + private fun getSimState(slotIndex: Int, teleMgr: Any?, teleClass: Class<*>?): Int? { + return try { + getValue("getSimState", slotIndex, teleMgr, teleClass) as Int? + } catch (e: Exception) { + TelephonyManager.SIM_STATE_UNKNOWN + } + } + + private fun getDeviceId(slotIndex: Int, teleMgr: Any?, teleClass: Class<*>?): String? { + var imei = getValue("getDeviceId", slotIndex, teleMgr, teleClass) as String? + if (imei == null) imei = getValue("getImei", slotIndex, teleMgr, teleClass) as String? + return imei + } + + private fun getSimIccId(subscriptionId: Int, teleMgr: Any?, teleClass: Class<*>?): String? = + getValue("getSimSerialNumber", subscriptionId, teleMgr, teleClass) as String? + + fun getSimImsi(subscriptionId: Int, teleMgr: Any, teleClass: Class<*>): String? = + getValue("getSubscriberId", subscriptionId, teleMgr, teleClass) as String? + + private fun getTeleClass(teleMgr: Any?, className: String?): Class<*>? { + try { + if (className != null) return Class.forName(className) + } catch (ignored: ClassNotFoundException) { + } + return teleMgr?.javaClass + } + + private fun getValue( + methodName: String, slotIndex: Int, teleMgr: Any?, teleClass: Class<*>? + ): Any? { + var result: Any? = spamTeleMgr(methodName, teleMgr, teleClass, slotIndex) + if (result == null) result = spamTeleMgr(methodName, teleMgr, teleClass, null) + return result + } + + private fun spamTeleMgr( + methodName: String?, teleMgr: Any?, teleClass: Class<*>?, subscriptionId: Any? + ): Any? { + if (methodName == null || methodName.isEmpty()) return null + var result: Any? + //TODO - FIX ME +// for (methodSuffix in METHOD_SUFFIXES) { +// result = MultiSimWorker().runMethodReflect( +//// teleMgr, +// teleClass, +// methodName + methodSuffix, +// subscriptionId?.let { arrayOf(it) }) +// if (result != null) { +// return result +// } +// } + return null + } +} diff --git a/multisim/src/main/java/com/hover/multisim/sim/Utils.kt b/multisim/src/main/java/com/hover/multisim/sim/Utils.kt new file mode 100644 index 0000000..f65b80d --- /dev/null +++ b/multisim/src/main/java/com/hover/multisim/sim/Utils.kt @@ -0,0 +1,37 @@ +package com.hover.multisim.sim + +import android.Manifest +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.os.Build +import io.sentry.Sentry + +object Utils { + + private const val SHARED_PREFS = "_multisim" + + @JvmStatic + fun getPackage(context: Context): String? { + return try { + context.applicationContext.packageName + } catch (e: NullPointerException) { + Sentry.captureException(e) + null + } + } + + /* ktlint-disable max-line-length */ + @JvmStatic + fun hasPhonePerm(context: Context): Boolean { + return Build.VERSION.SDK_INT < 23 || context.checkSelfPermission(Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED && context.checkSelfPermission( + Manifest.permission.READ_PHONE_STATE + ) == PackageManager.PERMISSION_GRANTED + } + + fun getSharedPrefs(context: Context): SharedPreferences { + return context.getSharedPreferences( + getPackage(context) + SHARED_PREFS, Context.MODE_PRIVATE + ) + } +} diff --git a/multisim/src/main/res/values/colors.xml b/multisim/src/main/res/values/colors.xml new file mode 100644 index 0000000..a491964 --- /dev/null +++ b/multisim/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #008577 + #00574B + #D81B60 + diff --git a/multisim/src/main/res/values/strings.xml b/multisim/src/main/res/values/strings.xml new file mode 100644 index 0000000..68e3975 --- /dev/null +++ b/multisim/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + MultiSim + diff --git a/multisim/src/test/java/com/hover/multisim/data/database/dao/SimDaoTest.kt b/multisim/src/test/java/com/hover/multisim/data/database/dao/SimDaoTest.kt new file mode 100644 index 0000000..e5072e7 --- /dev/null +++ b/multisim/src/test/java/com/hover/multisim/data/database/dao/SimDaoTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2022 UseHover + * + * 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.hover.multisim.data.database.dao + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import com.appmattus.kotlinfixture.kotlinFixture +import com.hover.multisim.data.database.Database +import com.hover.multisim.data.database.model.HSDKSims +import java.io.IOException +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.MatcherAssert +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class SimDaoTest { + + private lateinit var simDao: SimDao + private lateinit var db: Database + private val fixture = kotlinFixture() + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, Database::class.java + ).build() + simDao = db.simDao() + } + + @After + @Throws(IOException::class) + fun tearDown() { + db.close() + } + + @Test + fun `test simDao fetches all sim data`() = runTest { + val sim: HSDKSims = fixture() + simDao.insert(sim) + val result = simDao.getAll().first() + MatcherAssert.assertThat(sim.imsi, `is`(result.first().imsi)) + } +} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index d9c5076..0000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -include ':app' -rootProject.name='MultiSim' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..5511627 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +mapOf( + "app" to "app", +).forEach { (projectName, projectPath) -> + include(":$projectName") + project(":$projectName").projectDir = File(projectPath) +} + +include(":multisim") + +rootProject.name = "MultiSim" diff --git a/spotless/copyright.kt b/spotless/copyright.kt new file mode 100644 index 0000000..2cf820d --- /dev/null +++ b/spotless/copyright.kt @@ -0,0 +1,15 @@ +/* + * Copyright $YEAR UseHover + * + * 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. + */ \ No newline at end of file