diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..728e6bc
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+magnifier
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..fb7f4a8
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..a5f05cd
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..860da66
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..797acea
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..7d41f00
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,65 @@
+# Contribution Guidelines
+
+Thank you for your interest in Verifiable Credentials!
+
+This project welcomes contributions and suggestions. Most contributions require you to
+agree to a Contributor License Agreement (CLA) declaring that you have the right to,
+and actually do, grant us the rights to use your contribution.
+
+For details, visit https://cla.microsoft.com.
+
+When you submit a pull request, a CLA-bot will automatically determine whether you need
+to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the
+instructions provided by the bot. You will only need to do this once across all repositories using our CLA.
+
+This project has adopted the Microsoft Open Source Code of Conduct.
+For more information see the Code of Conduct FAQ
+or contact opencode@microsoft.com with any additional questions or comments.
+
+Contributions come in many forms: submitting issues, writing code, participating in discussions and community calls.
+
+This document provides the guidelines for how to contribute to the Verifiable Credentials Wallet SDK project.
+
+## Issues
+
+This section describes the guidelines for submitting issues
+
+### Issue Types
+
+There are 4 types of issues:
+
+- Issue/Bug: You've found a bug with the code, and want to report it, or create an issue to track the bug.
+- Issue/Discussion: You have something on your mind, which requires input form others in a discussion, before it eventually manifests as a proposal.
+- Issue/Proposal: Used for items that propose a new idea or functionality. This allows feedback from others before code is written.
+- Issue/Question: Use this issue type, if you need help or have a question.
+
+## Contributing
+
+This section describes the guidelines for contributing code / docs to the project.
+
+### Pull Requests
+
+All contributions come through pull requests. To submit a proposed change, we recommend following this workflow:
+
+1. Make sure there's an issue (bug or proposal) raised, which sets the expectations for the contribution you are about to make.
+2. Fork the relevant repo and create a new branch
+3. Create your change
+ - Code changes require tests
+4. Update relevant documentation for the change
+5. Commit and open a PR
+6. Wait for the CI process to finish and make sure all checks are green
+7. A maintainer of the project will be assigned, and you can expect a review within a few days
+
+#### Use work-in-progress PRs for early feedback
+
+A good way to communicate before investing too much time is to create a "Work-in-progress" PR and share it with your reviewers. The standard way of doing this is to add a "[WIP]" prefix in your PR's title and assign the **do-not-merge** label. This will let people looking at your PR know that it is not well baked yet.
+
+### Use of Third-party code
+
+- Third-party code must include licenses.
+
+**Thank You!** - Your contributions to open source, large or small, make projects like this possible. Thank you for taking the time to contribute.
+
+## Code of Conduct
+
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
\ No newline at end of file
diff --git a/README.md b/README.md
index 5cd7cec..cfe6007 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,119 @@
-# Project
+# OMagnifier
-> This repo has been populated by an initial template to help get you started. Please
-> make sure to update the content to build a great experience for community-building.
+OMagnifier is an Android APM SDK that can be used to monitor the app performance.
+
+## Features Support
+- [x] Frame rate monitor: floating window for showing fps
+- [x] Memory usage metrics monitor: monitor and collect the memeory usage metrics
+- [ ] Memory usage viewer
+- [ ] Battery usage monitor
+
+## APIs
+### Frame rate monitor
+
+1. Start frame rate monitor
+```kotlin
+Magnifier.startMonitorFPS(
+ FPSMonitorConfig.Builder(this.application)
+ .lowPercentage(40 / 60f) // show red tips, (2.0f / 3.0f) by default
+ .mediumPercentage(50 / 60f) // show yellow tips, (5.0f / 6.0f) by default
+ .refreshRate(60f) // defaultDisplay.refreshRate by default
+ .build()
+)
+```
+
+2. Stop frame rate monitor
+```kotlin
+Magnifier.stopMonitorFPS()
+```
+
+### Memory usage monitor
+
+The mectrics we support now:
+
+- `HeapMemoryInfo`: heap memory and vss/pss memory
+- `FileDescriptorInfo`: file readlink and max open file count
+- `ThreadInfo`: thread count and thread stack trace
+
+1. Start Memory usage metrics monitor
+
+```kotlin
+MemoryMonitorConfig.Builder()
+ .enableExceedLimitSample(0.8f, // the benchmark for Exceed_Limit type sampler, if we reach out 80% the max, collect the metrics, 0.8f by default
+ 10000 // the threshold for Exceed_Limit type sampler, 10s by default
+ )
+ .enableTimingSample(60 * 1000) // threshold for the timing checker, 1 min by default
+ .onSampleListener(object : MemoryMonitor.OnSampleListener {
+ override fun onSampleHeap(
+ heapMemoryInfo: HeapMemoryInfo,
+ sampleType: MemoryMonitor.SampleType
+ ) {
+ Log.d(TAG, "heapMemoryInfo:$heapMemoryInfo,sampleType:$sampleType")
+ }
+
+ override fun onSampleFile(
+ fileDescriptorInfo: FileDescriptorInfo,
+ sampleType: MemoryMonitor.SampleType
+ ) {
+ Log.d(TAG, "fileDescriptorInfo:${fileDescriptorInfo.fdMaxCount},sampleType:$sampleType")
+ }
+
+ override fun onSampleThread(
+ threadInfo: ThreadInfo,
+ sampleType: MemoryMonitor.SampleType
+ ) {
+ Log.d(TAG, "threadInfo:${threadInfo.threadsCount},sampleType:$sampleType")
+ }
+ }).build()
+```
+
+2. Collect the memory usage metrics immdiately
+
+```koltin
+Magnifier.dumpMemoryImmediately(object : MemoryMonitor.OnSampleListener {
+ override fun onSampleHeap(
+ heapMemoryInfo: HeapMemoryInfo,
+ sampleType: MemoryMonitor.SampleType
+ ) {
+ Log.d(TAG, "heapMemoryInfo:$heapMemoryInfo,sampleType:$sampleType")
+ }
+
+ override fun onSampleFile(
+ fileDescriptorInfo: FileDescriptorInfo,
+ sampleType: MemoryMonitor.SampleType
+ ) {
+ Log.d(TAG, "fileDescriptorInfo:${fileDescriptorInfo.fdMaxCount},sampleType:$sampleType")
+ }
+
+ override fun onSampleThread(
+ threadInfo: ThreadInfo,
+ sampleType: MemoryMonitor.SampleType
+ ) {
+ Log.d(TAG, "threadInfo:${threadInfo.threadsCount},sampleType:$sampleType")
+ }
+})
+```
+
+
+3. Stop frame rate monitor
+
+```kotlin
+Magnifier.stopMonitorMemory()
+```
+
+## Demo
+
+The demo is under Module app.
+
+1. Install the app
+2. Run the app
+3. Click the button for testing
-As the maintainer of this project, please make a few updates:
-- Improving this README.MD file to provide a great experience
-- Updating SUPPORT.MD with content about this project's support experience
-- Understanding the security reporting process in SECURITY.MD
-- Remove this section from the README
## Contributing
-This project welcomes contributions and suggestions. Most contributions require you to agree to a
+This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
@@ -24,10 +125,8 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
-## Trademarks
+## License
-This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
-trademarks or logos is subject to and must follow
-[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
-Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
-Any use of third-party trademarks or logos are subject to those third-party's policies.
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Licensed under the [MIT](LICENSE) license.
\ No newline at end of file
diff --git a/SECURITY.md b/SECURITY.md
index f7b8998..e0dfff5 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,10 +1,10 @@
-
+
## Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
-If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.
+If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below.
## Reporting Security Issues
@@ -12,9 +12,9 @@ If you believe you have found a security vulnerability in any Microsoft-owned re
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
-If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
+If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
-You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
+You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
@@ -38,4 +38,4 @@ We prefer all communications to be in English.
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
-
\ No newline at end of file
+
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..6adf66d
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion "30.0.3"
+
+ defaultConfig {
+ applicationId "com.microsoft.office.outlook.magnifier"
+ minSdkVersion 16
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation 'androidx.core:core-ktx:1.3.2'
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'com.google.android.material:material:1.3.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ testImplementation 'junit:junit:4.+'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+ implementation project(':magnifierlib')
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /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.
+#
+# 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/microsoft/office/outlook/magnifier/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/microsoft/office/outlook/magnifier/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..39d0ece
--- /dev/null
+++ b/app/src/androidTest/java/com/microsoft/office/outlook/magnifier/ExampleInstrumentedTest.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifier
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * 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.microsoft.office.outlook.magnifier", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ea03564
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/microsoft/office/outlook/magnifier/MainActivity.kt b/app/src/main/java/com/microsoft/office/outlook/magnifier/MainActivity.kt
new file mode 100644
index 0000000..394710c
--- /dev/null
+++ b/app/src/main/java/com/microsoft/office/outlook/magnifier/MainActivity.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifier
+
+import android.app.Activity
+import android.os.Bundle
+import android.util.Log
+import android.widget.Button
+import com.microsoft.office.outlook.magnifierlib.Magnifier
+import com.microsoft.office.outlook.magnifierlib.frame.FPSMonitorConfig
+import com.microsoft.office.outlook.magnifierlib.memory.FileDescriptorInfo
+import com.microsoft.office.outlook.magnifierlib.memory.HeapMemoryInfo
+import com.microsoft.office.outlook.magnifierlib.memory.MemoryMonitor
+import com.microsoft.office.outlook.magnifierlib.memory.MemoryMonitorConfig
+import com.microsoft.office.outlook.magnifierlib.memory.ThreadInfo
+
+class MainActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ val buttonStartFPS = findViewById(R.id.button_fps_start)
+ val buttonStopFPS = findViewById(R.id.button_fps_stop)
+ val buttonSleep = findViewById(R.id.button_fps_sleep)
+ val buttonStartMemory = findViewById(R.id.button_start_memory)
+ val buttonStopMemory = findViewById(R.id.button_stop_memory)
+ val buttonIncreaseMemory = findViewById(R.id.button_increase_memory)
+ val buttonDumpMemory = findViewById(R.id.button_dump_memory_immediately)
+
+ buttonStartFPS.setOnClickListener {
+ Magnifier.startMonitorFPS(
+ FPSMonitorConfig.Builder(this.application).lowPercentage(40 / 60f) // show red tips, (2.0f / 3.0f) by default
+ .mediumPercentage(50 / 60f) // show yellow tips, (5.0f / 6.0f) by default
+ .refreshRate(60f) // defaultDisplay.refreshRate by default
+ .build()
+ )
+ Log.i(TAG, "after startMonitorFPS isEnabledFPSMonitor: ${Magnifier.isEnabledFPSMonitor()}")
+ }
+
+ buttonStopFPS.setOnClickListener {
+ Magnifier.stopMonitorFPS()
+ Log.i(TAG, "after stopMonitorFPS isEnabledFPSMonitor: ${Magnifier.isEnabledFPSMonitor()}")
+ }
+
+ buttonSleep.setOnClickListener {
+ Thread.sleep(100)
+ }
+
+ buttonStartMemory.setOnClickListener {
+ Log.i(TAG, "before startMonitorMemory isEnableMemoryMonitor: ${Magnifier.isEnabledMemoryMonitor()}")
+ Magnifier.startMonitorMemory(
+ MemoryMonitorConfig.Builder().enableExceedLimitSample(
+ 0.8f, // the benchmark for Exceed_Limit type sampler, if we reach out 80% the max, collect the metrics, 0.8f by default
+ 10000 // the threshold for Exceed_Limit type sampler, 10s by default
+ ).enableTimingSample(60 * 1000) // threshold for the timing checker, 1 min by default
+ .onSampleListener(object : MemoryMonitor.OnSampleListener {
+ override fun onSampleHeap(
+ heapMemoryInfo: HeapMemoryInfo,
+ sampleType: MemoryMonitor.SampleType
+ ) {
+ Log.d(TAG, "heapMemoryInfo:$heapMemoryInfo,sampleType:$sampleType")
+ }
+
+ override fun onSampleFile(
+ fileDescriptorInfo: FileDescriptorInfo,
+ sampleType: MemoryMonitor.SampleType
+ ) {
+ Log.d(TAG, "fileDescriptorInfo:${fileDescriptorInfo.fdMaxCount},sampleType:$sampleType")
+ }
+
+ override fun onSampleThread(
+ threadInfo: ThreadInfo,
+ sampleType: MemoryMonitor.SampleType
+ ) {
+ Log.d(TAG, "threadInfo:${threadInfo.threadsCount},sampleType:$sampleType")
+ }
+ }).build()
+ )
+ Log.i(TAG, "after startMonitorMemory isEnableMemoryMonitor: ${Magnifier.isEnabledMemoryMonitor()}")
+ }
+
+ buttonStopMemory.setOnClickListener {
+ Log.i(TAG, "before stopMonitorMemory isEnableMemoryMonitor: ${Magnifier.isEnabledMemoryMonitor()}")
+ Magnifier.stopMonitorMemory()
+ Log.i(TAG, "after stopMonitorMemory isEnableMemoryMonitor: ${Magnifier.isEnabledMemoryMonitor()}")
+ }
+
+ buttonIncreaseMemory.setOnClickListener {
+ val list = ArrayList()
+ Thread {
+ val used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
+ val totalFree = Runtime.getRuntime().maxMemory() - used
+ val arr = ByteArray((totalFree * 0.85).toInt()){
+ 1
+ }
+ list.add(arr)
+ }.start()
+ }
+
+ buttonDumpMemory.setOnClickListener {
+ Magnifier.dumpMemoryImmediately(object : MemoryMonitor.OnSampleListener {
+ override fun onSampleHeap(
+ heapMemoryInfo: HeapMemoryInfo,
+ sampleType: MemoryMonitor.SampleType
+ ) {
+ Log.d(TAG, "heapMemoryInfo:$heapMemoryInfo,sampleType:$sampleType")
+ }
+
+ override fun onSampleFile(
+ fileDescriptorInfo: FileDescriptorInfo,
+ sampleType: MemoryMonitor.SampleType
+ ) {
+ Log.d(TAG, "fileDescriptorInfo:${fileDescriptorInfo.fdMaxCount},sampleType:$sampleType")
+ }
+
+ override fun onSampleThread(
+ threadInfo: ThreadInfo,
+ sampleType: MemoryMonitor.SampleType
+ ) {
+ Log.d(TAG, "threadInfo:${threadInfo.threadsCount},sampleType:$sampleType")
+ }
+ })
+ }
+ }
+
+ companion object {
+ private const val TAG = "MainActivity"
+ }
+}
\ No newline at end of file
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..247a89f
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..8496a39
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..5ff8d4b
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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..71ad2fd
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ 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..71ad2fd
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a571e60
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c41dd28
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dba46d
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ac681
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f25a419
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png 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..1b2215b
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..61bef36
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,14 @@
+
+
+
+ #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
new file mode 100644
index 0000000..20e2454
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+ Magnifier
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..3787b6f
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/microsoft/office/outlook/magnifier/ExampleUnitTest.kt b/app/src/test/java/com/microsoft/office/outlook/magnifier/ExampleUnitTest.kt
new file mode 100644
index 0000000..c353cd9
--- /dev/null
+++ b/app/src/test/java/com/microsoft/office/outlook/magnifier/ExampleUnitTest.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifier
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * 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)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..13cbb4c
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+buildscript {
+ ext.kotlin_version = "1.4.21"
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:4.1.2"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+
+ // 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
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..98bed16
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a2ac5d4
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Apr 20 19:32:15 CST 2021
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/magnifierlib/.gitignore b/magnifierlib/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/magnifierlib/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/magnifierlib/build.gradle b/magnifierlib/build.gradle
new file mode 100644
index 0000000..7cf93ce
--- /dev/null
+++ b/magnifierlib/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+apply from: './publish.gradle'
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion "30.0.3"
+
+ defaultConfig {
+ minSdkVersion 16
+ targetSdkVersion 30
+ versionCode Integer.parseInt(System.getProperty("versionCode", "1"))
+ versionName System.getProperty("versionName", project.VERSION_NAME)
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation 'androidx.core:core-ktx:1.3.2'
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'com.google.android.material:material:1.3.0'
+ testImplementation 'junit:junit:4.+'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+}
\ No newline at end of file
diff --git a/magnifierlib/consumer-rules.pro b/magnifierlib/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/magnifierlib/gradle.properties b/magnifierlib/gradle.properties
new file mode 100644
index 0000000..854cd36
--- /dev/null
+++ b/magnifierlib/gradle.properties
@@ -0,0 +1,3 @@
+POM_GROUP_ID=com.microsoft.office.outlook
+POM_ARTIFACT_ID=magnifier
+VERSION_NAME=0.0.7-SNAPSHOT
diff --git a/magnifierlib/proguard-rules.pro b/magnifierlib/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/magnifierlib/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.
+#
+# 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/magnifierlib/publish.gradle b/magnifierlib/publish.gradle
new file mode 100644
index 0000000..3306665
--- /dev/null
+++ b/magnifierlib/publish.gradle
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+apply plugin: 'maven-publish'
+
+task androidJavadocs(type: Javadoc) {
+ failOnError false
+ source = android.sourceSets.main.java.sourceFiles
+
+ classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+
+}
+
+task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
+ from androidJavadocs.destinationDir
+ classifier = 'javadoc'
+}
+
+task androidSourcesJar(type: Jar) {
+ from android.sourceSets.main.java.srcDirs
+ classifier = 'sources'
+}
+
+
+publishing {
+ publications {
+ magnifier(MavenPublication) {
+ afterEvaluate {
+ groupId project.POM_GROUP_ID
+ artifactId project.POM_ARTIFACT_ID
+ version System.getProperty("versionName", project.VERSION_NAME)
+ artifact bundleReleaseAar
+ artifact androidJavadocsJar
+ artifact androidSourcesJar
+ }
+
+ pom.withXml {
+ def dependenciesNode = asNode().appendNode('dependencies')
+
+ configurations.implementation.getAllDependencies().each { Dependency dep ->
+ if (dep.group == null
+ || dep.version == null
+ || dep.name == null
+ || dep.name == "unspecified") {
+ return // ignore invalid dependencies
+ }
+
+ def dependencyNode = dependenciesNode.appendNode('dependency')
+ dependencyNode.appendNode('groupId', dep.group)
+ dependencyNode.appendNode('artifactId', dep.name)
+ dependencyNode.appendNode('version', dep.version)
+
+ if (!dep.transitive) {
+ // If this dependency is transitive, we should force exclude
+ // all its dependencies them from the POM
+ def exclusionNode = dependencyNode.appendNode('exclusions').appendNode('exclusion')
+ exclusionNode.appendNode('groupId', '*')
+ exclusionNode.appendNode('artifactId', '*')
+ } else if (!dep.properties.excludeRules.empty) {
+ // Otherwise add specified exclude rules
+ def exclusionsNode = dependencyNode.appendNode('exclusions')
+ dep.properties.excludeRules.each { ExcludeRule rule ->
+ def exclusionNode = exclusionsNode.appendNode('exclusion')
+ exclusionNode.appendNode('groupId', rule.group ?: '*')
+ exclusionNode.appendNode('artifactId', rule.module ?: '*')
+ }
+ }
+ }
+ }
+ }
+ }
+
+ repositories {
+ maven {
+ url 'https://office.pkgs.visualstudio.com/_packaging/OutlookMobile/maven/v1'
+ credentials {
+ // TODO: replace this local.properties after set up pipelines in VSTS
+ Properties properties = new Properties()
+ properties.load(project.rootProject.file('local.properties').newDataInputStream())
+ username = properties.getProperty('vsoUserName')
+ password = properties.getProperty('vsoPassword')
+ }
+ }
+ }
+}
+
diff --git a/magnifierlib/src/androidTest/java/com/microsoft/office/outlook/magnifierlib/ExampleInstrumentedTest.kt b/magnifierlib/src/androidTest/java/com/microsoft/office/outlook/magnifierlib/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..b82c184
--- /dev/null
+++ b/magnifierlib/src/androidTest/java/com/microsoft/office/outlook/magnifierlib/ExampleInstrumentedTest.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * 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.microsoft.office.outlook.magnifierlib.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/magnifierlib/src/main/AndroidManifest.xml b/magnifierlib/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..e203469
--- /dev/null
+++ b/magnifierlib/src/main/AndroidManifest.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/Magnifier.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/Magnifier.kt
new file mode 100644
index 0000000..c55bb53
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/Magnifier.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib
+
+import androidx.annotation.UiThread
+import com.microsoft.office.outlook.magnifierlib.frame.FPSMonitor
+import com.microsoft.office.outlook.magnifierlib.frame.FPSMonitorConfig
+import com.microsoft.office.outlook.magnifierlib.memory.MemoryMonitor
+import com.microsoft.office.outlook.magnifierlib.memory.MemoryMonitorConfig
+
+object Magnifier {
+
+ private val fpsMonitor: FPSMonitor by lazy { FPSMonitor() }
+
+ private val memoryMonitor: MemoryMonitor by lazy { MemoryMonitor() }
+
+ @UiThread
+ fun startMonitorFPS(config: FPSMonitorConfig) {
+ fpsMonitor.startMonitorFPS(config)
+ }
+
+ @UiThread
+ fun stopMonitorFPS() {
+ fpsMonitor.stopMonitorFPS()
+ }
+
+ fun isEnabledFPSMonitor(): Boolean {
+ return fpsMonitor.isFPSMonitorEnabled()
+ }
+
+ fun startMonitorMemory(config: MemoryMonitorConfig) {
+ memoryMonitor.start(config)
+ }
+
+ fun stopMonitorMemory() {
+ memoryMonitor.stop()
+ }
+
+ fun dumpMemoryImmediately(onSampleListener: MemoryMonitor.OnSampleListener) {
+ memoryMonitor.dumpImmediately(onSampleListener)
+ }
+
+ fun isEnabledMemoryMonitor(): Boolean {
+ return memoryMonitor.isMonitorEnabled()
+ }
+}
\ No newline at end of file
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/Permissions.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/Permissions.kt
new file mode 100644
index 0000000..fa9ee29
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/Permissions.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.provider.Settings
+
+fun drawOverlaysPermission(context: Context): Boolean {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(context).also {
+ if (!it) {
+ val intent = Intent(
+ Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.packageName)
+ )
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ context.startActivity(intent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/frame/FPSMonitor.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/frame/FPSMonitor.kt
new file mode 100644
index 0000000..14e4cdc
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/frame/FPSMonitor.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib.frame
+
+import android.annotation.SuppressLint
+import androidx.annotation.UiThread
+import com.microsoft.office.outlook.magnifierlib.drawOverlaysPermission
+
+class FPSMonitor {
+
+ private var frameCalculator: FrameCalculator? = null
+
+ @SuppressLint("StaticFieldLeak")
+ private var frameViewer: FrameViewer? = null
+
+ @UiThread
+ @Synchronized
+ fun startMonitorFPS(config: FPSMonitorConfig) {
+ if (drawOverlaysPermission(config.context)) {
+ return
+ }
+
+ if (isFPSMonitorEnabled()) {
+ return
+ }
+
+ frameViewer = FrameViewer(config.context, config.refreshRate, config.mediumPercentage, config.lowPercentage)
+ frameCalculator = FrameCalculator {
+ frameViewer?.display(it)
+ }
+
+ frameViewer?.show()
+ frameCalculator?.start()
+ }
+
+ @UiThread
+ @Synchronized
+ fun stopMonitorFPS() {
+ if (!isFPSMonitorEnabled()) {
+ return
+ }
+
+ frameViewer?.hide()
+ frameCalculator?.stop()
+
+ frameViewer = null
+ frameCalculator = null
+ }
+
+ @Synchronized
+ fun isFPSMonitorEnabled(): Boolean {
+ return frameViewer != null && frameCalculator != null
+ }
+}
\ No newline at end of file
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/frame/FPSMonitorConfig.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/frame/FPSMonitorConfig.kt
new file mode 100644
index 0000000..5057df9
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/frame/FPSMonitorConfig.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib.frame
+
+import android.app.Application
+import android.app.Service
+import android.view.WindowManager
+
+class FPSMonitorConfig private constructor(
+ val context: Application,
+ val refreshRate: Float,
+ val mediumPercentage: Float,
+ val lowPercentage: Float
+) {
+
+ data class Builder(
+ val context: Application,
+ var mediumPercentage: Float,
+ var lowPercentage: Float,
+ var refreshRate: Float
+ ) {
+ constructor(context: Application) : this(
+ context,
+ FLAG_PERCENTAGE_YELLOW,
+ FLAG_PERCENTAGE_RED,
+ (context.getSystemService(Service.WINDOW_SERVICE) as WindowManager).defaultDisplay.refreshRate
+ )
+
+ fun mediumPercentage(mediumPercentage: Float) = apply { this.mediumPercentage = mediumPercentage }
+ fun lowPercentage(lowPercentage: Float) = apply { this.lowPercentage = lowPercentage }
+ fun refreshRate(refreshRate: Float) = apply { this.refreshRate = refreshRate }
+ fun build() = FPSMonitorConfig(context, refreshRate, mediumPercentage, lowPercentage)
+ }
+
+ companion object {
+ const val FLAG_PERCENTAGE_RED = 2 / 3f
+ const val FLAG_PERCENTAGE_YELLOW = 5 / 6f
+ }
+}
+
+
+
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/frame/FrameCalculator.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/frame/FrameCalculator.kt
new file mode 100644
index 0000000..1587393
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/frame/FrameCalculator.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib.frame
+
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Message
+import android.view.Choreographer
+import java.util.LinkedList
+
+class FrameCalculator(
+ private val listener: (frameCount: Int) -> Unit
+) : Choreographer.FrameCallback {
+
+ private val frameList = LinkedList()
+
+ private var handlerThread: HandlerThread = HandlerThread(MAGNIFIER_FRAME_CALLBACK_THREAD_NAME).also { it.start() }
+
+ private var handler: Handler = object : Handler(handlerThread.looper) {
+ override fun handleMessage(msg: Message) {
+ super.handleMessage(msg)
+ when (msg.what) {
+ HANDLE_MSG -> notifyListener()
+ }
+ }
+ }
+
+ override fun doFrame(frameTimeNanos: Long) {
+ frameList.add(frameTimeNanos)
+ Choreographer.getInstance().postFrameCallback(this)
+ }
+
+ fun start() {
+ Choreographer.getInstance().postFrameCallback(this)
+ frameList.clear()
+ handler.sendEmptyMessageDelayed(HANDLE_MSG, THRESHOLD_IN_MS)
+ }
+
+ fun stop() {
+ Choreographer.getInstance().removeFrameCallback(this)
+ frameList.clear()
+ handler.removeCallbacksAndMessages(null)
+ handlerThread.quit()
+ handlerThread.interrupt()
+ }
+
+ private fun notifyListener() {
+ listener.invoke(frameList.size)
+ frameList.clear()
+ handler.sendEmptyMessageDelayed(HANDLE_MSG, THRESHOLD_IN_MS)
+ }
+
+ companion object {
+ private const val HANDLE_MSG = 0
+ private const val THRESHOLD_IN_MS = 1000L
+ private const val MAGNIFIER_FRAME_CALLBACK_THREAD_NAME = "magnifier_frame_callback_thread"
+ }
+}
\ No newline at end of file
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/frame/FrameViewer.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/frame/FrameViewer.kt
new file mode 100644
index 0000000..7124c32
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/frame/FrameViewer.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib.frame
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.app.Service
+import android.content.Context
+import android.graphics.PixelFormat
+import android.os.Build
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnTouchListener
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.TextView
+import com.microsoft.office.outlook.magnifierlib.R
+
+@SuppressLint("ClickableViewAccessibility")
+class FrameViewer(
+ context: Application,
+ private val refreshRate: Float,
+ private val mediumPercentage: Float,
+ private val lowPercentage: Float
+) {
+ @SuppressLint("InflateParams")
+ private val textView: TextView = LayoutInflater.from(context).inflate(R.layout.frame_view, null) as TextView
+ private val windowManager: WindowManager = textView.context.getSystemService(Service.WINDOW_SERVICE) as WindowManager
+
+ init {
+
+ val minWidth: Int = (textView.lineHeight + textView.totalPaddingTop + textView.totalPaddingBottom + textView.paint.fontMetrics.bottom.toInt())
+ textView.minWidth = minWidth
+
+ val params = WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.RGBA_8888
+ )
+ params.gravity = DEFAULT_GRAVITY
+ params.x = POSITION_X
+ params.y = POSITION_L
+
+ windowManager.addView(textView, params)
+
+ textView.setOnTouchListener(MovingTouchListener(params, windowManager))
+
+ textView.isHapticFeedbackEnabled = false
+ }
+
+ fun display(frameCount: Int) {
+ textView.post {
+ when {
+ frameCount > refreshRate * mediumPercentage -> {
+ textView.setBackgroundResource(R.drawable.fps_good)
+ }
+ frameCount > refreshRate * lowPercentage -> {
+ textView.setBackgroundResource(R.drawable.fps_medium)
+ }
+ else -> {
+ textView.setBackgroundResource(R.drawable.fps_bad)
+ }
+ }
+ textView.text = frameCount.toString()
+ }
+ }
+
+ fun show() {
+ textView.visibility = View.VISIBLE
+ }
+
+ fun hide() {
+ textView.visibility = View.GONE
+ windowManager.removeView(textView)
+ }
+
+ class MovingTouchListener(
+ private val params: WindowManager.LayoutParams,
+ private val windowManager: WindowManager
+ ) : OnTouchListener {
+ private var initialX = 0
+ private var initialY = 0
+ private var initialTouchX = 0f
+ private var initialTouchY = 0f
+ override fun onTouch(
+ v: View,
+ event: MotionEvent
+ ): Boolean {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ initialX = params.x
+ initialY = params.y
+ initialTouchX = event.rawX
+ initialTouchY = event.rawY
+ }
+ MotionEvent.ACTION_MOVE -> {
+ params.x = initialX + (event.rawX - initialTouchX).toInt()
+ params.y = initialY + (event.rawY - initialTouchY).toInt()
+ windowManager.updateViewLayout(v, params)
+ }
+ }
+ return false
+ }
+ }
+
+ companion object {
+ const val DEFAULT_GRAVITY = Gravity.TOP or Gravity.START
+ const val POSITION_X = 200
+ const val POSITION_L = 600
+ }
+}
\ No newline at end of file
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/FileDescriptorMetricCollector.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/FileDescriptorMetricCollector.kt
new file mode 100644
index 0000000..76ef77d
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/FileDescriptorMetricCollector.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib.memory
+
+class FileDescriptorMetricCollector : IMetricCollector {
+
+ override fun collect(): FileDescriptorInfo {
+ return FileDescriptorInfo(readMaxOpenFiles(), readFileDescriptors())
+ }
+}
+
+data class FileDescriptorInfo(
+ val fdMaxCount: Long,
+ val fileDescriptors: List
+)
\ No newline at end of file
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/HeapMetricCollector.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/HeapMetricCollector.kt
new file mode 100644
index 0000000..b1ea6d2
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/HeapMetricCollector.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib.memory
+
+import android.os.Debug
+
+class HeapMetricCollector : IMetricCollector {
+
+ override fun collect(): HeapMemoryInfo {
+ return HeapMemoryInfo(
+ maxMemoryMB = Runtime.getRuntime().maxMemory() / M,
+ usedMemoryMB = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / M,
+ pssMemoryMB = Debug.getPss() / K,
+ vssMemoryMB = readVss() / K,
+ rssMemoryMB = readVmRss() / K
+ )
+ }
+
+ companion object {
+ private const val K = 1024
+ private const val M = K * K
+ }
+}
+
+data class HeapMemoryInfo(
+ val maxMemoryMB: Long,
+ val usedMemoryMB: Long,
+ val pssMemoryMB: Long,
+ val vssMemoryMB: Long,
+ val rssMemoryMB: Long
+)
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemoryMonitor.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemoryMonitor.kt
new file mode 100644
index 0000000..aca4ed6
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemoryMonitor.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib.memory
+
+import android.os.Handler
+import android.os.HandlerThread
+
+class MemoryMonitor {
+
+ private val handlerThread: HandlerThread = HandlerThread(HANDLER_THREAD_MEMORY_MONITOR).apply { this.start() }
+
+ private val handler: Handler = Handler(handlerThread.looper)
+
+ private val factory = MemorySamplersFactory()
+
+ private var syncSamplerList: ArrayList> = ArrayList()
+
+ private var asyncSamplerList: ArrayList> = ArrayList()
+
+ @Volatile
+ private var isMonitorEnabled = false
+
+ @Synchronized
+ fun start(config: MemoryMonitorConfig) {
+
+ if (isMonitorEnabled) {
+ return
+ }
+
+ isMonitorEnabled = true
+
+ asyncSamplerList = factory.createSamplers(config, handler)
+
+ for (memorySampler in asyncSamplerList) {
+ memorySampler.start()
+ }
+ }
+
+ @Synchronized
+ fun stop() {
+
+ if (!isMonitorEnabled) {
+ return
+ }
+
+ isMonitorEnabled = false
+
+ for (memorySampler in asyncSamplerList) {
+ memorySampler.stop()
+ }
+
+ asyncSamplerList.clear()
+ }
+
+ @Synchronized
+ fun dumpImmediately(onSampleListener: OnSampleListener) {
+
+ if (syncSamplerList.isEmpty()) {
+ syncSamplerList = factory.createSamplers(
+ MemoryMonitorConfig.Builder(MemoryMonitorConfig.MemoryMonitorType.SYNC).onSampleListener(onSampleListener).build(), handler
+ )
+ }
+
+ for (memorySampler in syncSamplerList) {
+ memorySampler.start()
+ }
+ }
+
+ @Synchronized
+ fun isMonitorEnabled(): Boolean {
+ return isMonitorEnabled
+ }
+
+ interface IMemoryMonitor {
+
+ fun start()
+
+ fun stop()
+ }
+
+ interface OnSampleListener {
+
+ fun onSampleHeap(
+ heapMemoryInfo: HeapMemoryInfo,
+ sampleType: SampleType
+ )
+
+ fun onSampleFile(
+ fileDescriptorInfo: FileDescriptorInfo,
+ sampleType: SampleType
+ )
+
+ fun onSampleThread(
+ threadInfo: ThreadInfo,
+ sampleType: SampleType
+ )
+ }
+
+ enum class SampleType {
+ TIMING, EXCEED_LIMIT, IMMEDIATE
+ }
+
+ companion object {
+ private const val HANDLER_THREAD_MEMORY_MONITOR = "magnifier_memory"
+ }
+}
\ No newline at end of file
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemoryMonitorConfig.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemoryMonitorConfig.kt
new file mode 100644
index 0000000..09d2eac
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemoryMonitorConfig.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib.memory
+
+class MemoryMonitorConfig private constructor(
+ val monitorType: MemoryMonitorType,
+ val openTimingSample: Boolean,
+ val openExceedLimitSample: Boolean,
+ val timingThreshold: Long,
+ val exceedLimitRatio: Float,
+ val exceedLimitThreshold: Long,
+ val onSampleListener: MemoryMonitor.OnSampleListener?
+) {
+
+ data class Builder(
+ val monitorType: MemoryMonitorType,
+ var openTimingSample: Boolean = false,
+ var openExceedLimitSample: Boolean = false,
+ var timingThreshold: Long = DEFAULT_TIMING_THRESHOLD_1_MIN,
+ var exceedLimitRatio: Float = DEFAULT_EXCEED_LIMIT_RATIO_80_PERCENTAGE,
+ var exceedLimitThreshold: Long = DEFAULT_EXCEED_LIMIT_RATIO_THRESHOLD_10_SECONDS,
+ var onSampleListener: MemoryMonitor.OnSampleListener? = null
+ ) {
+
+ constructor(monitorType: MemoryMonitorType = MemoryMonitorType.ASYNC) : this(
+ monitorType,
+ false,
+ false,
+ DEFAULT_TIMING_THRESHOLD_1_MIN,
+ DEFAULT_EXCEED_LIMIT_RATIO_80_PERCENTAGE,
+ DEFAULT_EXCEED_LIMIT_RATIO_THRESHOLD_10_SECONDS,
+ null
+ )
+
+ fun enableTimingSample(
+ timingThreshold: Long
+ ) = apply {
+ this.openTimingSample = true
+ this.timingThreshold = timingThreshold
+ }
+
+ fun enableExceedLimitSample(
+ exceedLimitRatio: Float,
+ exceedLimitThreshold: Long
+ ) = apply {
+ this.openExceedLimitSample = true
+ this.exceedLimitRatio = exceedLimitRatio
+ this.exceedLimitThreshold = exceedLimitThreshold
+ }
+
+ fun onSampleListener(onSampleListener: MemoryMonitor.OnSampleListener) = apply { this.onSampleListener = onSampleListener }
+
+ fun build() =
+ MemoryMonitorConfig(monitorType, openTimingSample, openExceedLimitSample, timingThreshold, exceedLimitRatio, exceedLimitThreshold, onSampleListener)
+ }
+
+ enum class MemoryMonitorType {
+ ASYNC, SYNC
+ }
+
+ companion object {
+ private const val DEFAULT_TIMING_THRESHOLD_1_MIN = 1000 * 60L
+ private const val DEFAULT_EXCEED_LIMIT_RATIO_80_PERCENTAGE = 0.8f
+ private const val DEFAULT_EXCEED_LIMIT_RATIO_THRESHOLD_10_SECONDS = 1000 * 10L
+ }
+}
\ No newline at end of file
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemorySampler.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemorySampler.kt
new file mode 100644
index 0000000..9ed61d9
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemorySampler.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib.memory
+
+import android.os.Handler
+
+class MemorySampler(
+ private val collector: IMetricCollector,
+ private val policy: ISamplePolicy,
+ private val handler: Handler,
+ private val onSampleListener: (t: T) -> Unit
+) : Runnable, MemoryMonitor.IMemoryMonitor {
+
+ override fun run() {
+ if (policy.needSample()) {
+ val sample = collector.collect()
+ onSampleListener.invoke(sample)
+ }
+ if (policy.needPostDelayed()) {
+ handler.postDelayed(this, policy.postDelayedThreshold())
+ }
+ }
+
+ override fun start() {
+ handler.post(this)
+ }
+
+ override fun stop() {
+ handler.removeCallbacks(this)
+ }
+}
+
+interface IMetricCollector {
+ fun collect(): T
+}
+
+interface ISamplePolicy {
+ fun needSample(): Boolean
+
+ fun needPostDelayed(): Boolean
+
+ fun postDelayedThreshold(): Long
+}
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemorySamplersFactory.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemorySamplersFactory.kt
new file mode 100644
index 0000000..a149aae
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemorySamplersFactory.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib.memory
+
+import android.os.Handler
+
+class MemorySamplersFactory {
+
+ fun createSamplers(
+ config: MemoryMonitorConfig,
+ handler: Handler
+ ): ArrayList> {
+
+ return when (config.monitorType) {
+ MemoryMonitorConfig.MemoryMonitorType.ASYNC -> createAsyncSamplers(config, handler)
+ MemoryMonitorConfig.MemoryMonitorType.SYNC -> createSyncSamplers(config, handler)
+ }
+ }
+
+ private fun createAsyncSamplers(
+ config: MemoryMonitorConfig,
+ handler: Handler
+ ): ArrayList> {
+
+ val list = ArrayList>()
+
+ if (config.openTimingSample) {
+
+ val timingHeapSampler = MemorySampler(HeapMetricCollector(), TimingSamplePolicy(config.timingThreshold), handler) {
+ config.onSampleListener?.onSampleHeap(it, MemoryMonitor.SampleType.TIMING)
+ }
+
+ val timingFileSampler = MemorySampler(FileDescriptorMetricCollector(), TimingSamplePolicy(config.timingThreshold), handler) {
+ config.onSampleListener?.onSampleFile(it, MemoryMonitor.SampleType.TIMING)
+ }
+
+ val timingThreadSampler = MemorySampler(ThreadMetricCollector(), TimingSamplePolicy(config.timingThreshold), handler) {
+ config.onSampleListener?.onSampleThread(it, MemoryMonitor.SampleType.TIMING)
+ }
+
+ list.add(timingHeapSampler)
+ list.add(timingFileSampler)
+ list.add(timingThreadSampler)
+ }
+
+ // create the top hit samplers
+ if (config.openExceedLimitSample) {
+
+ // build the top hit heap sampler
+ val topHitHeapNeedSampleFunc: () -> Boolean = {
+ (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()).toFloat() >= Runtime.getRuntime()
+ .maxMemory() * config.exceedLimitRatio
+ }
+ val topHitHeapSampler = MemorySampler(
+ HeapMetricCollector(), ExceedLimitSamplePolicy(config.exceedLimitThreshold, topHitHeapNeedSampleFunc), handler
+ ) {
+ config.onSampleListener?.onSampleHeap(it, MemoryMonitor.SampleType.EXCEED_LIMIT)
+ }
+
+ // build the top hit file sampler
+ val topHitFileNeedSampleFunc: () -> Boolean = { readFileDescriptors().size.toFloat() >= readMaxOpenFiles() * config.exceedLimitRatio }
+ val topHitFileSampler =
+ MemorySampler(FileDescriptorMetricCollector(), ExceedLimitSamplePolicy(config.exceedLimitThreshold, topHitFileNeedSampleFunc), handler) {
+ config.onSampleListener?.onSampleFile(it, MemoryMonitor.SampleType.EXCEED_LIMIT)
+ }
+
+ // build the top hit thread sampler
+ val topHitThreadNeedSampleFunc: () -> Boolean = { readThreadsCount() >= MAX_THREADS_COUNT * config.exceedLimitRatio }
+ val topHitThreadSampler =
+ MemorySampler(ThreadMetricCollector(), ExceedLimitSamplePolicy(config.exceedLimitThreshold, topHitThreadNeedSampleFunc), handler) {
+ config.onSampleListener?.onSampleThread(it, MemoryMonitor.SampleType.EXCEED_LIMIT)
+ }
+
+ list.add(topHitHeapSampler)
+ list.add(topHitFileSampler)
+ list.add(topHitThreadSampler)
+ }
+
+ return list
+ }
+
+ private fun createSyncSamplers(
+ config: MemoryMonitorConfig,
+ handler: Handler
+ ): ArrayList> {
+
+ val list = ArrayList>()
+
+ // for just dumping once
+ val immediateHeapSampler = MemorySampler(HeapMetricCollector(), ImmediateSamplePolicy(), handler) {
+ config.onSampleListener?.onSampleHeap(it, MemoryMonitor.SampleType.IMMEDIATE)
+ }
+
+ val immediateFileSampler = MemorySampler(FileDescriptorMetricCollector(), ImmediateSamplePolicy(), handler) {
+ config.onSampleListener?.onSampleFile(it, MemoryMonitor.SampleType.IMMEDIATE)
+ }
+
+ val immediateThreadSampler = MemorySampler(ThreadMetricCollector(), ImmediateSamplePolicy(), handler) {
+ config.onSampleListener?.onSampleThread(it, MemoryMonitor.SampleType.IMMEDIATE)
+ }
+
+ list.add(immediateHeapSampler)
+ list.add(immediateFileSampler)
+ list.add(immediateThreadSampler)
+
+ return list
+ }
+
+ companion object {
+ private const val MAX_THREADS_COUNT = 1000
+ }
+}
\ No newline at end of file
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemoryUtils.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemoryUtils.kt
new file mode 100644
index 0000000..d8a0e82
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/MemoryUtils.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+@file:JvmName("MemoryUtils")
+
+package com.microsoft.office.outlook.magnifierlib.memory
+
+import android.os.Build
+import android.os.Process
+import android.system.Os
+import java.io.File
+import java.io.RandomAccessFile
+import java.util.ArrayList
+import java.util.regex.Pattern
+
+private const val INDEX_PROC_STATUS = 1
+private const val INDEX_PROC_LIMITS = 3
+
+fun readVss(): Long {
+ return readFieldFromProcFile("VmSize", INDEX_PROC_STATUS, "/proc/${Process.myPid()}/status")
+}
+
+fun readVmRss(): Long {
+ return readFieldFromProcFile("VmRSS", INDEX_PROC_STATUS, "/proc/${Process.myPid()}/status")
+}
+
+fun readThreadsCount(): Long {
+ return readFieldFromProcFile("Threads", INDEX_PROC_STATUS, "/proc/${Process.myPid()}/status")
+}
+
+fun readMaxOpenFiles(): Long {
+ return readFieldFromProcFile("Max open files", INDEX_PROC_LIMITS, "/proc/${Process.myPid()}/limits")
+}
+
+fun readFileDescriptors(): List {
+ return ArrayList().also {
+ val listFiles: Array = File("proc/${Process.myPid()}/fd").listFiles() ?: arrayOf()
+ for (file in listFiles) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ try {
+ it.add(Os.readlink(file.absolutePath))
+ } catch (swallowed: Exception) {
+ }
+ }
+ }
+ }
+}
+
+private fun readFieldFromProcFile(
+ fieldName: String,
+ fieldIndex: Int,
+ fileName: String
+): Long {
+ val file = RandomAccessFile(fileName, "r")
+ file.use {
+ var line = file.readLine()
+ while (line != null) {
+ if (line.startsWith(fieldName)) {
+ val arr = Pattern.compile("\\s+").split(line, 0)
+ if (arr.size > 1) {
+ return arr[fieldIndex].toLong()
+ }
+ }
+ line = file.readLine()
+ }
+ }
+ return 0
+}
+
+
+
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/SamplePolicy.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/SamplePolicy.kt
new file mode 100644
index 0000000..32eef7d
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/SamplePolicy.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib.memory
+
+class TimingSamplePolicy(private val threshold: Long) : ISamplePolicy {
+ override fun needSample(): Boolean {
+ return true
+ }
+
+ override fun needPostDelayed(): Boolean {
+ return true
+ }
+
+ override fun postDelayedThreshold(): Long {
+ return threshold
+ }
+}
+
+class ImmediateSamplePolicy() : ISamplePolicy {
+ override fun needSample(): Boolean {
+ return true
+ }
+
+ override fun needPostDelayed(): Boolean {
+ return false
+ }
+
+ override fun postDelayedThreshold(): Long {
+ return Long.MAX_VALUE
+ }
+}
+
+class ExceedLimitSamplePolicy(
+ private val threshold: Long,
+ private val needSampleFunc: () -> Boolean
+) : ISamplePolicy {
+ override fun needSample(): Boolean {
+ return needSampleFunc.invoke()
+ }
+
+ override fun needPostDelayed(): Boolean {
+ return true
+ }
+
+ override fun postDelayedThreshold(): Long {
+ return threshold
+ }
+}
\ No newline at end of file
diff --git a/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/ThreadMetricCollector.kt b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/ThreadMetricCollector.kt
new file mode 100644
index 0000000..d395c26
--- /dev/null
+++ b/magnifierlib/src/main/java/com/microsoft/office/outlook/magnifierlib/memory/ThreadMetricCollector.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib.memory
+
+class ThreadMetricCollector : IMetricCollector {
+
+ override fun collect(): ThreadInfo {
+ val allStackTraces = Thread.getAllStackTraces()
+ return ThreadInfo(readThreadsCount(), allStackTraces)
+ }
+}
+
+data class ThreadInfo(
+ val threadsCount: Long,
+ val threadMap: Map>
+)
\ No newline at end of file
diff --git a/magnifierlib/src/main/res/drawable/fps_bad.xml b/magnifierlib/src/main/res/drawable/fps_bad.xml
new file mode 100644
index 0000000..b4ff06a
--- /dev/null
+++ b/magnifierlib/src/main/res/drawable/fps_bad.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/magnifierlib/src/main/res/drawable/fps_good.xml b/magnifierlib/src/main/res/drawable/fps_good.xml
new file mode 100644
index 0000000..d7335d4
--- /dev/null
+++ b/magnifierlib/src/main/res/drawable/fps_good.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/magnifierlib/src/main/res/drawable/fps_medium.xml b/magnifierlib/src/main/res/drawable/fps_medium.xml
new file mode 100644
index 0000000..fa9b3ac
--- /dev/null
+++ b/magnifierlib/src/main/res/drawable/fps_medium.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/magnifierlib/src/main/res/layout/frame_view.xml b/magnifierlib/src/main/res/layout/frame_view.xml
new file mode 100644
index 0000000..7d7eac2
--- /dev/null
+++ b/magnifierlib/src/main/res/layout/frame_view.xml
@@ -0,0 +1,15 @@
+
+
+
\ No newline at end of file
diff --git a/magnifierlib/src/main/res/values/dimens.xml b/magnifierlib/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..2599d8a
--- /dev/null
+++ b/magnifierlib/src/main/res/values/dimens.xml
@@ -0,0 +1,8 @@
+
+
+
+ 4dp
+
\ No newline at end of file
diff --git a/magnifierlib/src/test/java/com/microsoft/office/outlook/magnifierlib/ExampleUnitTest.kt b/magnifierlib/src/test/java/com/microsoft/office/outlook/magnifierlib/ExampleUnitTest.kt
new file mode 100644
index 0000000..a8b8341
--- /dev/null
+++ b/magnifierlib/src/test/java/com/microsoft/office/outlook/magnifierlib/ExampleUnitTest.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.office.outlook.magnifierlib
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * 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)
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..95ee8eb
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,3 @@
+include ':magnifierlib'
+include ':app'
+rootProject.name = "magnifier"
\ No newline at end of file