Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • akwizgran/hotspot
  • grote/hotspot
  • briar/hotspot
3 results
Show changes
Commits on Source (84)
Showing
with 1624 additions and 187 deletions
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleSettingsManager">
<option name="PER_PROJECT_SETTINGS">
<value>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
<option name="RIGHT_MARGIN" value="100" />
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
</AndroidXmlCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="ANNOTATION_PARAMETER_WRAP" value="1" />
</JavaCodeStyleSettings>
<Objective-C-extensions>
<option name="GENERATE_INSTANCE_VARIABLES_FOR_PROPERTIES" value="ASK" />
<option name="RELEASE_STYLE" value="IVAR" />
<option name="TYPE_QUALIFIERS_PLACEMENT" value="BEFORE" />
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" />
<pair source="c" header="h" />
</extensions>
</Objective-C-extensions>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="Groovy">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="RIGHT_MARGIN" value="80" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_RESOURCES" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="RESOURCE_LIST_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_LIST_WRAP" value="1" />
<option name="EXTENDS_KEYWORD_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<option name="ASSERT_STATEMENT_WRAP" value="1" />
<option name="PARAMETER_ANNOTATION_WRAP" value="1" />
<option name="VARIABLE_ANNOTATION_WRAP" value="1" />
<option name="ENUM_CONSTANTS_WRAP" value="1" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_NAMESPACE>Namespace:</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_NAMESPACE>Namespace:</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</value>
</option>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</component>
</project>
\ No newline at end of file
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="ANNOTATION_PARAMETER_WRAP" value="1" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value />
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="Groovy">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="RIGHT_MARGIN" value="80" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_RESOURCES" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="RESOURCE_LIST_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_LIST_WRAP" value="1" />
<option name="EXTENDS_KEYWORD_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<option name="ASSERT_STATEMENT_WRAP" value="1" />
<option name="PARAMETER_ANNOTATION_WRAP" value="1" />
<option name="VARIABLE_ANNOTATION_WRAP" value="1" />
<option name="ENUM_CONSTANTS_WRAP" value="1" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
<arrangement>
<rules>
......@@ -112,5 +194,11 @@
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="PARAMETER_ANNOTATION_WRAP" value="1" />
<option name="VARIABLE_ANNOTATION_WRAP" value="1" />
<option name="ENUM_CONSTANTS_WRAP" value="1" />
</codeStyleSettings>
</code_scheme>
</component>
\ No newline at end of file
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SaveActionSettings">
<option name="actions">
<set>
<option value="activate" />
<option value="organizeImports" />
<option value="reformat" />
</set>
</option>
<option name="configurationPath" value="" />
</component>
</project>
\ No newline at end of file
......@@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "org.briarproject.hotspot"
minSdkVersion 16
......@@ -23,8 +23,11 @@ android {
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.fragment:fragment:1.3.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'com.google.zxing:core:3.4.0'
implementation 'org.nanohttpd:nanohttpd:2.3.1'
implementation 'org.jsoup:jsoup:1.11.3'
}
......@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="org.briarproject.hotspot">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
......
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
background-color: #F2F2F2;
font-family: Roboto,Arial,Helvetica,sans-serif;
font-size: 14px;
margin: 0;
height: 100%;
}
div#top {
background-color: #FFFFFF;
padding: 16px;
}
div#bottom {
padding: 16px 32px;
margin-top: 12px;
}
a.button {
background-color: #82C91E;
width: 100%;
display: block;
box-sizing: border-box;
padding: 12px 32px !important;
border: 1px solid transparent;
border-radius: 2px;
color: #000000 !important;
cursor: pointer;
font-weight: 500;
text-decoration: none;
text-transform: uppercase;
text-align: center;
margin: 20px auto 20px auto;
}
ol {
list-style: none;
counter-reset: briar-counter;
padding-left: 40px;
}
ol li {
counter-increment: briar-counter;
margin-bottom: 2em;
}
ol li::before {
content: counter(briar-counter);
background-color: #82C91E;
color: #000000 !important;
font-weight: bold;
border-radius: 70px;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
position: absolute;
left: 32px;
}
</style>
</head>
<body>
<div id="top">
<svg style="width:156px;height:47px;" viewBox="0 0 778 235">
<path style="fill:#87c214"
d="m 64.900391,0 c -9.7,0 -17.701172,7.9992183 -17.701172,17.699219 v 22.5 h 43.601562 v -22.5 C 90.800781,7.9992183 82.899219,0 73.199219,0 Z m 96.999999,0 c -9.7,0 -17.70117,7.9992183 -17.70117,17.699219 V 137.19922 h 43.60156 V 17.699219 C 187.80078,7.9992183 179.89922,0 170.19922,0 Z M 47.199219,97.800781 V 217.30078 c 0,9.7 7.901172,17.69922 17.701172,17.69922 h 8.298828 c 9.7,0 17.701172,-7.99922 17.701172,-17.69922 V 97.800781 Z m 97.000001,96.999999 v 22.5 c 0,9.7 8.00117,17.69922 17.70117,17.69922 h 8.29883 c 9.7,0 17.70117,-7.99922 17.70117,-17.69922 v -22.5 z"/>
<path style="fill:#95d220"
d="M 17.699219,47.199219 C 7.9992186,47.199219 0,55.100391 0,64.900391 v 8.298828 c 0,9.7 7.8992186,17.701172 17.699219,17.701172 H 137.19922 V 47.199219 Z m 177.101561,0 v 43.701172 h 22.5 c 9.7,0 17.69922,-7.901172 17.69922,-17.701172 v -8.298828 c 0,-9.8 -7.99922,-17.701172 -17.69922,-17.701172 z M 17.699219,144.19922 C 7.9992186,144.19922 0,152.10039 0,161.90039 v 8.29883 c 0,9.7 7.8992186,17.70117 17.699219,17.70117 h 22.5 v -43.70117 z m 80.101562,0 v 43.70117 H 217.30078 c 9.7,0 17.69922,-8.00117 17.69922,-17.70117 v -8.29883 c 0,-9.8 -7.99922,-17.70117 -17.69922,-17.70117 z"/>
<path d="M 301,60.564864 V 174.43514 h 53.31362 c 25.13729,0 38.31622,-12.58548 38.31622,-32.27441 0,-12.78766 -5.88,-22.32687 -17.63776,-27.60431 v -0.20217 c 8.91968,-5.48043 12.77339,-12.38249 12.77339,-23.140374 0,-16.238294 -11.14945,-30.648991 -34.66495,-30.648991 z m 110.68683,0 V 174.43514 h 13.37598 v -45.67022 l -1.41529,-1.41926 h 26.95811 c 15.00127,0 23.51842,5.27428 28.99185,17.04704 l 14.1887,30.04244 h 15.00139 l -16.82503,-35.52128 c -3.64896,-7.91617 -9.52848,-12.99064 -14.79921,-15.22341 v -0.20216 c 12.36593,-3.24765 22.70429,-14.41228 22.70429,-29.229734 0,-22.530633 -17.43208,-33.693671 -38.31224,-33.693671 z m 111.08726,0 V 174.43514 h 13.37992 V 60.564864 Z m 78.65821,0 -50.07469,113.870276 h 14.59701 l 12.16287,-27.40213 -0.60656,-1.41926 h 62.2336 l -0.60655,1.41926 12.16286,27.40213 h 14.59701 L 615.62098,60.564864 Z m 79.463,0 V 174.43514 h 13.37994 v -45.67022 l -1.41927,-1.41926 h 26.96209 c 15.00128,0 23.51842,5.27428 28.99185,17.04704 l 14.1887,30.04244 H 778 l -16.82503,-35.52128 c -3.64895,-7.91617 -9.52851,-12.99064 -14.79921,-15.22341 v -0.20216 c 12.36591,-3.24765 22.70427,-14.41228 22.70427,-29.229734 0,-22.530633 -17.43209,-33.693671 -38.31223,-33.693671 z M 312.96068,73.147961 h 38.72057 c 14.59584,0 22.29593,5.887175 22.29593,18.065895 0,10.148944 -6.07834,18.268094 -22.29593,18.268094 h -38.72057 l 1.41927,-1.41927 V 74.571187 Z m 110.68684,0 h 37.90786 c 13.78495,0 24.32519,5.684988 24.52791,20.908395 0,12.178724 -9.52687,20.702244 -25.94718,20.702244 h -36.48859 l 1.41529,-1.41927 V 74.571187 Z m 269.00626,0 h 37.90788 c 13.98769,0 24.53187,5.684988 24.53187,20.908395 0,12.178724 -9.52688,20.702244 -25.94718,20.702244 h -36.49257 l 1.41927,-1.41927 V 74.571187 Z m -83.92693,1.423226 h 0.20615 l 3.44509,11.366019 20.06794,45.670224 1.41924,1.41926 h -50.07071 l 1.41926,-1.41926 20.06793,-45.670224 z M 312.96068,122.06505 h 41.35294 c 16.82575,0 24.53189,7.71398 24.53189,20.09568 0,12.58468 -7.09797,19.69131 -24.53189,19.69131 h -41.35294 l 1.41927,-1.42322 v -36.94055 z"/>
</svg>
<h2 id="download_title">Download Briar 1.2.20</h2>
<span id="download_intro">Someone nearby shared Briar with you.</span>
<a href="/app.apk" class="button">
<svg aria-hidden="true" style="width:24px;height:24px;margin-right:6px;vertical-align:middle;"
viewBox="0 0 24 24">
<path fill="currentColor" d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/>
</svg>
<span id="download_button">Download Briar</span>
</a>
<span id="download_outro">After the download is complete, open the downloaded file and install it.</span>
</div>
<div id="bottom">
<h3 id="troubleshooting_title">Troubleshooting</h3>
<ol>
<li id="troubleshooting_1">If you can't download the app, try it with a different web
browser app.
</li>
<li id="troubleshooting_2">Ensure that your browser is allowed to download apps directly by
giving it the permission or enabling the installation of apps from "Unknown Sources" in
system settings.
</li>
</ol>
</div>
</body>
</html>
package org.briarproject.hotspot;
import android.net.wifi.WifiManager;
import androidx.core.util.Consumer;
import androidx.fragment.app.FragmentActivity;
import static android.content.Context.WIFI_SERVICE;
/**
* Abstract base class for the ConditionManagers that ensure that the conditions
* to open a hotspot are fulfilled. There are different extensions of this for
* API levels lower than 29 and 29+.
*/
abstract class AbstractConditionManager {
enum Permission {
UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
}
protected final Consumer<Boolean> permissionUpdateCallback;
protected FragmentActivity ctx;
protected WifiManager wifiManager;
AbstractConditionManager(Consumer<Boolean> permissionUpdateCallback) {
this.permissionUpdateCallback = permissionUpdateCallback;
}
/**
* Pass a FragmentActivity context here during `onCreateView()`.
*/
void init(FragmentActivity ctx) {
this.ctx = ctx;
this.wifiManager = (WifiManager) ctx.getApplicationContext()
.getSystemService(WIFI_SERVICE);
}
/**
* Call this during onStart() in the fragment where the ConditionManager
* is used.
*/
abstract void onStart();
/**
* Check if all required conditions are met such that the hotspot can be
* started. If any precondition is not met yet, bring up relevant dialogs
* asking the user to grant relevant permissions or take relevant actions.
*
* @return true if conditions are fulfilled and flow can continue.
*/
abstract boolean checkAndRequestConditions();
}
package org.briarproject.hotspot;
import android.content.Intent;
import android.provider.Settings;
import java.util.logging.Logger;
import androidx.activity.result.ActivityResultCaller;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.core.util.Consumer;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.hotspot.UiUtils.showRationale;
/**
* This class ensures that the conditions to open a hotspot are fulfilled on
* API levels < 29.
* <p>
* As soon as {@link #checkAndRequestConditions()} returns true,
* all conditions are fulfilled.
*/
class ConditionManager extends AbstractConditionManager {
private static final Logger LOG =
getLogger(ConditionManager.class.getName());
private final ActivityResultLauncher<Intent> wifiRequest;
ConditionManager(ActivityResultCaller arc,
Consumer<Boolean> permissionUpdateCallback) {
super(permissionUpdateCallback);
wifiRequest = arc.registerForActivityResult(
new StartActivityForResult(),
result -> permissionUpdateCallback
.accept(wifiManager.isWifiEnabled()));
}
@Override
void onStart() {
// nothing to do here
}
private boolean areEssentialPermissionsGranted() {
if (LOG.isLoggable(INFO)) {
LOG.info(String.format("areEssentialPermissionsGranted(): " +
"wifiManager.isWifiEnabled()? %b",
wifiManager.isWifiEnabled()));
}
return wifiManager.isWifiEnabled();
}
@Override
boolean checkAndRequestConditions() {
if (areEssentialPermissionsGranted()) return true;
if (!wifiManager.isWifiEnabled()) {
// Try enabling the Wifi and return true if that seems to have been
// successful, i.e. "Wifi is either already in the requested state, or
// in progress toward the requested state".
if (wifiManager.setWifiEnabled(true)) {
LOG.info("Enabled wifi");
return true;
}
// Wifi is not enabled and we can't seem to enable it, so ask the user
// to enable it for us.
showRationale(ctx, R.string.wifi_settings_title,
R.string.wifi_settings_request_enable_body,
this::requestEnableWiFi,
() -> permissionUpdateCallback.accept(false));
}
return false;
}
private void requestEnableWiFi() {
wifiRequest.launch(new Intent(Settings.ACTION_WIFI_SETTINGS));
}
}
package org.briarproject.hotspot;
import android.content.Intent;
import android.provider.Settings;
import java.util.logging.Logger;
import androidx.activity.result.ActivityResultCaller;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.util.Consumer;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
import static java.lang.Boolean.TRUE;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.hotspot.UiUtils.getGoToSettingsListener;
import static org.briarproject.hotspot.UiUtils.showDenialDialog;
import static org.briarproject.hotspot.UiUtils.showRationale;
/**
* This class ensures that the conditions to open a hotspot are fulfilled on
* API levels >= 29.
* <p>
* As soon as {@link #checkAndRequestConditions()} returns true,
* all conditions are fulfilled.
*/
@RequiresApi(29)
class ConditionManager29 extends AbstractConditionManager {
private static final Logger LOG =
getLogger(ConditionManager29.class.getName());
private Permission locationPermission = Permission.UNKNOWN;
private final ActivityResultLauncher<String> locationRequest;
private final ActivityResultLauncher<Intent> wifiRequest;
ConditionManager29(ActivityResultCaller arc,
Consumer<Boolean> permissionUpdateCallback) {
super(permissionUpdateCallback);
locationRequest = arc.registerForActivityResult(
new RequestPermission(), granted -> {
onRequestPermissionResult(granted);
permissionUpdateCallback.accept(TRUE.equals(granted));
});
wifiRequest = arc.registerForActivityResult(
new StartActivityForResult(),
result -> permissionUpdateCallback
.accept(wifiManager.isWifiEnabled()));
}
@Override
void onStart() {
locationPermission = Permission.UNKNOWN;
}
private boolean areEssentialPermissionsGranted() {
if (LOG.isLoggable(INFO)) {
LOG.info(String.format("areEssentialPermissionsGranted(): " +
"locationPermission? %s, " +
"wifiManager.isWifiEnabled()? %b",
locationPermission,
wifiManager.isWifiEnabled()));
}
return locationPermission == Permission.GRANTED &&
wifiManager.isWifiEnabled();
}
@Override
boolean checkAndRequestConditions() {
if (areEssentialPermissionsGranted()) return true;
if (locationPermission == Permission.UNKNOWN) {
locationRequest.launch(ACCESS_FINE_LOCATION);
return false;
}
// If the location permission has been permanently denied, ask the
// user to change the setting
if (locationPermission == Permission.PERMANENTLY_DENIED) {
showDenialDialog(ctx, R.string.permission_location_title,
R.string.permission_hotspot_location_denied_body,
getGoToSettingsListener(ctx),
() -> permissionUpdateCallback.accept(false));
return false;
}
// Should we show the rationale for location permission?
if (locationPermission == Permission.SHOW_RATIONALE) {
showRationale(ctx, R.string.permission_location_title,
R.string.permission_hotspot_location_request_body,
this::requestPermissions,
() -> permissionUpdateCallback.accept(false));
return false;
}
// If Wifi is not enabled, we show the rationale for enabling Wifi?
if (!wifiManager.isWifiEnabled()) {
showRationale(ctx, R.string.wifi_settings_title,
R.string.wifi_settings_request_enable_body,
this::requestEnableWiFi,
() -> permissionUpdateCallback.accept(false));
return false;
}
// we shouldn't usually reach this point, but if we do, return false
// anyway to force a recheck. Maybe some condition changed in the
// meantime.
return false;
}
private void onRequestPermissionResult(@Nullable Boolean granted) {
if (granted != null && granted) {
locationPermission = Permission.GRANTED;
} else if (shouldShowRequestPermissionRationale(ctx,
ACCESS_FINE_LOCATION)) {
locationPermission = Permission.SHOW_RATIONALE;
} else {
locationPermission = Permission.PERMANENTLY_DENIED;
}
}
private void requestPermissions() {
locationRequest.launch(ACCESS_FINE_LOCATION);
}
private void requestEnableWiFi() {
wifiRequest.launch(new Intent(Settings.Panel.ACTION_WIFI));
}
}
package org.briarproject.hotspot;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.hotspot.HotspotState.HotspotError;
import org.briarproject.hotspot.HotspotState.HotspotStarted;
import org.briarproject.hotspot.HotspotState.HotspotStopped;
import org.briarproject.hotspot.HotspotState.NetworkConfig;
import org.briarproject.hotspot.HotspotState.StartingHotspot;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import static android.os.Build.VERSION.SDK_INT;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.hotspot.HotspotManager.UNKNOWN_FREQUENCY;
import static org.briarproject.hotspot.QrCodeUtils.createQrCode;
import static org.briarproject.hotspot.QrCodeUtils.createWifiLoginString;
public class HotspotFragment extends Fragment {
private MainViewModel viewModel;
private ImageView qrCode;
private TextView ssidView, passwordView, statusView;
private Button button, serverButton;
private boolean hotspotStarted = false;
private final AbstractConditionManager conditionManager = SDK_INT < 29 ?
new ConditionManager(this, this::onPermissionUpdate) :
new ConditionManager29(this, this::onPermissionUpdate);
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
viewModel = new ViewModelProvider(requireActivity())
.get(MainViewModel.class);
conditionManager.init(requireActivity());
return inflater.inflate(R.layout.fragment_hotspot, container, false);
}
@Override
public void onViewCreated(@NonNull View v,
@Nullable Bundle savedInstanceState) {
super.onViewCreated(v, savedInstanceState);
qrCode = v.findViewById(R.id.qr_code);
ssidView = v.findViewById(R.id.ssid);
passwordView = v.findViewById(R.id.password);
statusView = v.findViewById(R.id.status);
button = v.findViewById(R.id.button);
button.setOnClickListener(this::onButtonClick);
serverButton = v.findViewById(R.id.serverButton);
serverButton.setOnClickListener(this::onServerButtonClick);
viewModel.getIs5GhzSupported().observe(getViewLifecycleOwner(),
b -> statusView
.setText(getString(R.string.wifi_5ghz_supported)));
viewModel.getStatus().observe(getViewLifecycleOwner(), state -> {
if (state instanceof StartingHotspot) {
statusView.setText(getString(R.string.starting_hotspot));
} else if (state instanceof HotspotStarted) {
onHotspotStarted((HotspotStarted) state);
} else if (state instanceof HotspotStopped) {
onHotspotStopped();
} else if (state instanceof HotspotError) {
onHotspotError((HotspotError) state);
}
});
}
private void onHotspotStarted(HotspotStarted state) {
button.setText(R.string.stop_hotspot);
button.setEnabled(true);
hotspotStarted = true;
NetworkConfig config = state.getConfig();
if (config.frequency == UNKNOWN_FREQUENCY)
statusView.setText(getString(R.string.start_callback_started));
else
statusView.setText(getString(R.string.start_callback_started_freq,
config.frequency));
String qrCodeText = createWifiLoginString(config.ssid,
config.password);
// TODO: heavy operation should be handed off to worker thread,
// potentially within the view model and provide it together
// with NetworkConfig?
Bitmap qrCodeBitmap = createQrCode(
getResources().getDisplayMetrics(), qrCodeText);
if (qrCodeBitmap == null) {
qrCode.setVisibility(GONE);
} else {
qrCode.setImageBitmap(qrCodeBitmap);
qrCode.setVisibility(VISIBLE);
}
ssidView.setText(getString(R.string.ssid, config.ssid));
passwordView.setText(getString(R.string.password, config.password));
serverButton.setVisibility(VISIBLE);
}
private void onHotspotStopped() {
qrCode.setVisibility(GONE);
ssidView.setText("");
passwordView.setText("");
button.setText(R.string.start_hotspot);
button.setEnabled(true);
hotspotStarted = false;
statusView.setText(getString(R.string.hotspot_stopped));
serverButton.setVisibility(GONE);
}
private void onHotspotError(HotspotError state) {
onHotspotStopped();
statusView.setText(state.getError());
}
@Override
public void onStart() {
super.onStart();
conditionManager.onStart();
}
private void onButtonClick(View view) {
button.setEnabled(false);
if (hotspotStarted) {
// the hotspot is currently started → stop it
viewModel.stopWifiP2pHotspot();
} else {
// the hotspot is currently stopped → start it
startWifiP2pHotspotIfConditionsFulfilled();
}
}
private void startWifiP2pHotspotIfConditionsFulfilled() {
if (conditionManager.checkAndRequestConditions()) {
viewModel.startWifiP2pHotspot();
}
}
private void onPermissionUpdate(boolean recheckPermissions) {
button.setEnabled(true);
if (recheckPermissions) {
startWifiP2pHotspotIfConditionsFulfilled();
}
}
public void onServerButtonClick(View view) {
getParentFragmentManager().beginTransaction()
.replace(R.id.fragment_container, new ServerFragment())
.addToBackStack(null)
.commit();
}
}
package org.briarproject.hotspot;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
import android.net.wifi.p2p.WifiP2pConfig;
import android.net.wifi.p2p.WifiP2pGroup;
import android.net.wifi.p2p.WifiP2pManager;
import android.net.wifi.p2p.WifiP2pManager.ActionListener;
import android.net.wifi.p2p.WifiP2pManager.Channel;
import android.os.Handler;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import org.briarproject.hotspot.HotspotState.NetworkConfig;
import java.util.logging.Logger;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.UiThread;
import static android.content.Context.POWER_SERVICE;
import static android.content.Context.WIFI_P2P_SERVICE;
import static android.content.Context.WIFI_SERVICE;
import static android.net.wifi.WifiManager.WIFI_MODE_FULL;
import static android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF;
import static android.net.wifi.p2p.WifiP2pConfig.GROUP_OWNER_BAND_2GHZ;
import static android.net.wifi.p2p.WifiP2pManager.BUSY;
import static android.net.wifi.p2p.WifiP2pManager.ERROR;
import static android.net.wifi.p2p.WifiP2pManager.NO_SERVICE_REQUESTS;
import static android.net.wifi.p2p.WifiP2pManager.P2P_UNSUPPORTED;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.PowerManager.FULL_WAKE_LOCK;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.hotspot.StringUtils.getRandomString;
class HotspotManager {
interface HotspotListener {
void onStartingHotspot();
void onHotspotStarted(NetworkConfig networkConfig);
void onDeviceConnected();
void onHotspotStopped();
void onHotspotError(String error);
}
private static final Logger LOG = getLogger(HotspotManager.class.getName());
private static final int MAX_FRAMEWORK_ATTEMPTS = 5;
private static final int MAX_GROUP_INFO_ATTEMPTS = 5;
private static final int RETRY_DELAY_MILLIS = 1000;
static final double UNKNOWN_FREQUENCY = Double.NEGATIVE_INFINITY;
private final Context ctx;
private final HotspotListener listener;
private final WifiManager wifiManager;
private final WifiP2pManager wifiP2pManager;
private final PowerManager powerManager;
private final Handler handler;
private final String lockTag;
@Nullable
// on API < 29 this is null because we cannot request a custom network name
private String networkName = null;
private WifiLock wifiLock;
private WakeLock wakeLock;
private Channel channel;
HotspotManager(Context ctx, HotspotListener listener) {
this.ctx = ctx;
this.listener = listener;
wifiManager = (WifiManager) ctx.getApplicationContext()
.getSystemService(WIFI_SERVICE);
wifiP2pManager =
(WifiP2pManager) ctx.getSystemService(WIFI_P2P_SERVICE);
powerManager = (PowerManager) ctx.getSystemService(POWER_SERVICE);
handler = new Handler(ctx.getMainLooper());
lockTag = ctx.getPackageName() + ":app-sharing-hotspot";
}
@UiThread
void startWifiP2pHotspot() {
if (wifiP2pManager == null) {
listener.onHotspotError(ctx.getString(R.string.no_wifi_direct));
return;
}
listener.onStartingHotspot();
acquireLocks();
startWifiP2pFramework(1);
}
/**
* As soon as Wifi is enabled, we try starting the WifiP2p framework.
* If Wifi has just been enabled, it is possible that will fail. If that
* happens we try again for MAX_FRAMEWORK_ATTEMPTS times after a delay of
* RETRY_DELAY_MILLIS after each attempt.
* <p>
* Rationale: it can take a few milliseconds for WifiP2p to become available
* after enabling Wifi. Depending on the API level it is possible to check this
* using {@link WifiP2pManager#requestP2pState} or register a BroadcastReceiver
* on the WIFI_P2P_STATE_CHANGED_ACTION to get notified when WifiP2p is really
* available. Trying to implement a solution that works reliably using these
* checks turned out to be a long rabbit-hole with lots of corner cases and
* workarounds for specific situations.
* Instead we now rely on this trial-and-error approach of just starting
* the framework and retrying if it fails.
* <p>
* We'll realize that the framework is busy when the ActionListener passed
* to {@link WifiP2pManager#createGroup} is called with onFailure(BUSY)
*/
void startWifiP2pFramework(int attempt) {
if (LOG.isLoggable(INFO))
LOG.info("startWifiP2pFramework attempt: " + attempt);
/*
* It is important that we call WifiP2pManager#initialize again
* for every attempt to starting the framework because otherwise,
* createGroup() will continue to fail with a BUSY state.
*/
channel = wifiP2pManager.initialize(ctx, ctx.getMainLooper(), null);
if (channel == null) {
listener.onHotspotError(ctx.getString(R.string.no_wifi_direct));
return;
}
ActionListener listener = new ActionListener() {
@Override
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
public void onSuccess() {
requestGroupInfo(1);
}
@Override
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
public void onFailure(int reason) {
LOG.info("onFailure: " + reason);
if (reason == BUSY) {
// WifiP2p not ready yet or hotspot already running
restartWifiP2pFramework(attempt);
} else if (reason == P2P_UNSUPPORTED) {
releaseHotspotWithError(ctx.getString(
R.string.start_callback_failed, "p2p unsupported"));
} else if (reason == ERROR) {
releaseHotspotWithError(ctx.getString(
R.string.start_callback_failed, "p2p error"));
} else if (reason == NO_SERVICE_REQUESTS) {
releaseHotspotWithError(ctx.getString(
R.string.start_callback_failed,
"no service requests"));
} else {
// all cases covered, in doubt set to error
releaseHotspotWithError(ctx.getString(
R.string.start_callback_failed_unknown, reason));
}
}
};
try {
if (SDK_INT >= 29) {
networkName = getNetworkName();
String passphrase = getPassphrase();
// TODO: maybe remove this in the production version
if (LOG.isLoggable(INFO))
LOG.info("networkName: " + networkName);
WifiP2pConfig config = new WifiP2pConfig.Builder()
.setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ)
.setNetworkName(networkName)
.setPassphrase(passphrase)
.build();
wifiP2pManager.createGroup(channel, config, listener);
} else {
wifiP2pManager.createGroup(channel, listener);
}
} catch (SecurityException e) {
// this should never happen, because we request permissions before
throw new AssertionError(e);
}
}
private void restartWifiP2pFramework(int attempt) {
LOG.info("retrying to start WifiP2p framework");
if (attempt < MAX_FRAMEWORK_ATTEMPTS) {
handler.postDelayed(() -> startWifiP2pFramework(attempt + 1),
RETRY_DELAY_MILLIS);
} else {
releaseHotspotWithError(
ctx.getString(R.string.stop_framework_busy));
}
}
@RequiresApi(29)
private String getNetworkName() {
return "DIRECT-" + getRandomString(2) + "-" +
getRandomString(10);
}
private String getPassphrase() {
return getRandomString(8);
}
@UiThread
void stopWifiP2pHotspot() {
if (channel == null) return;
wifiP2pManager.removeGroup(channel, new ActionListener() {
@Override
public void onSuccess() {
releaseHotspot();
}
@Override
public void onFailure(int reason) {
releaseHotspotWithError(ctx.getString(
R.string.stop_callback_failed, reason));
}
});
}
@SuppressLint("WakelockTimeout")
private void acquireLocks() {
// WIFI_MODE_FULL has no effect on API >= 29
int lockType =
SDK_INT >= 29 ? WIFI_MODE_FULL_HIGH_PERF : WIFI_MODE_FULL;
wifiLock = wifiManager.createWifiLock(lockType, lockTag);
wifiLock.acquire();
// FLAG_KEEP_SCREEN_ON is not respected on some Huawei devices.
wakeLock = powerManager.newWakeLock(FULL_WAKE_LOCK, lockTag);
wakeLock.acquire();
}
private void releaseHotspot() {
listener.onHotspotStopped();
closeChannelAndReleaseLock();
}
private void releaseHotspotWithError(String error) {
listener.onHotspotError(error);
closeChannelAndReleaseLock();
}
private void closeChannelAndReleaseLock() {
if (SDK_INT >= 27) channel.close();
channel = null;
wifiLock.release();
wakeLock.release();
}
private void requestGroupInfo(int attempt) {
if (LOG.isLoggable(INFO))
LOG.info("requestGroupInfo attempt: " + attempt);
WifiP2pManager.GroupInfoListener groupListener = group -> {
boolean valid = isGroupValid(group);
// If the group is valid, set the hotspot to started. If we don't
// have any attempts left and we have anything more or less usable,
// we try what we got
if (valid) {
// group is valid
onHotspotStarted(group);
} else if (attempt < MAX_GROUP_INFO_ATTEMPTS) {
// group invalid and we have attempts left
retryRequestingGroupInfo(attempt);
} else if (group != null) {
// no attempts left, try what we got
onHotspotStarted(group);
} else {
// no attempts left, group is null
releaseHotspotWithError(
ctx.getString(R.string.start_no_attempts_left));
}
};
try {
if (channel == null) return;
wifiP2pManager.requestGroupInfo(channel, groupListener);
} catch (SecurityException e) {
throw new AssertionError(e);
}
}
private void onHotspotStarted(WifiP2pGroup group) {
double frequency = UNKNOWN_FREQUENCY;
if (SDK_INT >= 29) {
frequency = ((double) group.getFrequency()) / 1000;
}
listener.onHotspotStarted(new NetworkConfig(
group.getNetworkName(), group.getPassphrase(),
frequency));
requestGroupInfoForConnection();
}
private void requestGroupInfoForConnection() {
if (LOG.isLoggable(INFO))
LOG.info("requestGroupInfo for connection");
WifiP2pManager.GroupInfoListener groupListener = group -> {
if (group == null || group.getClientList().isEmpty()) {
handler.postDelayed(this::requestGroupInfoForConnection,
RETRY_DELAY_MILLIS);
} else {
if (LOG.isLoggable(INFO)) {
LOG.info("client list " + group.getClientList());
}
listener.onDeviceConnected();
}
};
try {
if (channel == null) return;
wifiP2pManager.requestGroupInfo(channel, groupListener);
} catch (SecurityException e) {
throw new AssertionError(e);
}
}
private boolean isGroupValid(@Nullable WifiP2pGroup group) {
if (group == null) {
LOG.info("group is null");
return false;
} else if (!group.getNetworkName().startsWith("DIRECT-")) {
if (LOG.isLoggable(INFO)) {
LOG.info("received networkName without prefix 'DIRECT-': " +
group.getNetworkName());
}
return false;
} else if (networkName != null &&
!networkName.equals(group.getNetworkName())) {
if (LOG.isLoggable(INFO)) {
LOG.info("expected networkName: " + networkName);
LOG.info("received networkName: " + group.getNetworkName());
}
return false;
}
return true;
}
private void retryRequestingGroupInfo(int attempt) {
LOG.info("retrying to request group info");
// On some devices we need to wait for the group info to become available
if (attempt < MAX_GROUP_INFO_ATTEMPTS) {
handler.postDelayed(() -> requestGroupInfo(attempt + 1),
RETRY_DELAY_MILLIS);
} else {
releaseHotspotWithError(
ctx.getString(R.string.start_callback_no_group_info));
}
}
}
package org.briarproject.hotspot;
abstract class HotspotState {
static class StartingHotspot extends HotspotState {
}
static class NetworkConfig {
final String ssid, password;
final double frequency;
NetworkConfig(String ssid, String password, double frequency) {
this.ssid = ssid;
this.password = password;
this.frequency = frequency;
}
}
static class HotspotStarted extends HotspotState {
private final NetworkConfig config;
private final String url;
HotspotStarted(NetworkConfig config, String url) {
this.config = config;
this.url = url;
}
NetworkConfig getConfig() {
return config;
}
String getUrl() {
return url;
}
}
static class HotspotStopped extends HotspotState {
}
static class HotspotError extends HotspotState {
private final String error;
HotspotError(String error) {
this.error = error;
}
String getError() {
return error;
}
}
}
package org.briarproject.hotspot;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class InterfacesFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_interfaces, container, false);
}
@Override
public void onViewCreated(@NonNull View v,
@Nullable Bundle savedInstanceState) {
super.onViewCreated(v, savedInstanceState);
TextView textView = v.findViewById(R.id.text);
textView.setText(NetworkUtils.getNetworkInterfaceSummary());
}
}
package org.briarproject.hotspot;
import java.util.logging.Level;
import java.util.logging.Logger;
public class LogUtils {
public static void logException(Logger logger, Level level, Throwable t) {
if (logger.isLoggable(level)) logger.log(level, t.toString(), t);
}
}
package org.briarproject.hotspot;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION.SDK_INT;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.hotspot.QrCodeUtils.createQrCode;
import static org.briarproject.hotspot.QrCodeUtils.createWifiLoginString;
public class MainActivity extends AppCompatActivity {
private MainViewModel viewModel;
private ImageView qrCode;
private TextView ssidView, passwordView, statusView;
private Button button;
private boolean hotspotStarted = false;
MainViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
viewModel.setApplication(getApplication());
qrCode = findViewById(R.id.qr_code);
ssidView = findViewById(R.id.ssid);
passwordView = findViewById(R.id.password);
statusView = findViewById(R.id.status);
button = findViewById(R.id.button);
viewModel.getWifiConfiguration().observe(this, config -> {
if (config == null) {
qrCode.setVisibility(GONE);
ssidView.setText("");
passwordView.setText("");
button.setText(R.string.start_hotspot);
button.setEnabled(true);
hotspotStarted = false;
} else {
String qrCodeText = createWifiLoginString(config.ssid, config.password,
config.hidden);
Bitmap qrCodeBitmap = createQrCode(getResources().getDisplayMetrics(), qrCodeText);
if (qrCodeBitmap == null) {
qrCode.setVisibility(GONE);
} else {
qrCode.setImageBitmap(qrCodeBitmap);
qrCode.setVisibility(VISIBLE);
}
ssidView.setText(getString(R.string.ssid, config.ssid));
passwordView.setText(getString(R.string.password, config.password));
button.setText(R.string.stop_hotspot);
button.setEnabled(true);
hotspotStarted = true;
}
});
viewModel.getStatus().observe(this, status -> statusView.setText(status));
if (SDK_INT >= 29 && (checkSelfPermission(ACCESS_FINE_LOCATION) != PERMISSION_GRANTED)) {
requestPermissions(new String[]{ACCESS_FINE_LOCATION}, 0);
}
}
public void onButtonClick(View view) {
button.setEnabled(false);
if (hotspotStarted) viewModel.stopWifiP2pHotspot();
else viewModel.startWifiP2pHotspot();
}
}
package org.briarproject.hotspot;
import android.annotation.SuppressLint;
import android.app.Application;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
import android.net.wifi.p2p.WifiP2pManager;
import android.net.wifi.p2p.WifiP2pManager.ActionListener;
import android.net.wifi.p2p.WifiP2pManager.Channel;
import android.net.wifi.p2p.WifiP2pManager.GroupInfoListener;
import android.os.Handler;
import android.widget.Toast;
import org.briarproject.hotspot.HotspotState.HotspotError;
import org.briarproject.hotspot.HotspotState.HotspotStarted;
import org.briarproject.hotspot.HotspotState.HotspotStopped;
import org.briarproject.hotspot.HotspotState.NetworkConfig;
import org.briarproject.hotspot.HotspotState.StartingHotspot;
import java.util.logging.Logger;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import static android.content.Context.WIFI_P2P_SERVICE;
import static android.content.Context.WIFI_SERVICE;
import static android.net.wifi.WifiManager.WIFI_MODE_FULL;
import static android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF;
import static android.os.Build.VERSION.SDK_INT;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.hotspot.HotspotManager.HotspotListener;
import static org.briarproject.hotspot.WebServerManager.WebServerListener;
public class MainViewModel extends ViewModel {
public class MainViewModel extends AndroidViewModel
implements WebServerListener, HotspotListener {
private static final int MAX_GROUP_INFO_ATTEMPTS = 5;
private static final Logger LOG = getLogger(MainViewModel.class.getName());
private final MutableLiveData<NetworkConfig> config = new MutableLiveData<>();
private final MutableLiveData<String> status = new MutableLiveData<>();
private final MutableLiveData<Boolean> is5GhzSupported =
new MutableLiveData<>();
private Application app;
private String lockTag;
private WifiManager wifiManager;
private WifiP2pManager wifiP2pManager;
private Handler handler;
private final HotspotManager hotspotManager;
private final WebServerManager webServerManager;
private WifiLock wifiLock;
private Channel channel;
private final MutableLiveData<HotspotState> status =
new MutableLiveData<>();
void setApplication(Application app) {
this.app = app;
lockTag = app.getString(R.string.app_name);
wifiManager = (WifiManager) app.getSystemService(WIFI_SERVICE);
wifiP2pManager = (WifiP2pManager) app.getSystemService(WIFI_P2P_SERVICE);
handler = new Handler(app.getMainLooper());
}
public MainViewModel(@NonNull Application app) {
super(app);
hotspotManager = new HotspotManager(app, this);
webServerManager = new WebServerManager(app, this);
LiveData<NetworkConfig> getWifiConfiguration() {
return config;
if (SDK_INT >= 21) {
WifiManager wifiManager =
(WifiManager) app.getSystemService(WIFI_SERVICE);
if (wifiManager.is5GHzBandSupported()) {
is5GhzSupported.setValue(true);
}
}
}
LiveData<String> getStatus() {
LiveData<HotspotState> getStatus() {
return status;
}
void startWifiP2pHotspot() {
if (wifiP2pManager == null) {
status.setValue(app.getString(R.string.no_wifi_direct));
return;
}
status.setValue(app.getString(R.string.starting_hotspot));
channel = wifiP2pManager.initialize(app, app.getMainLooper(), null);
if (channel == null) {
status.setValue(app.getString(R.string.no_wifi_direct));
return;
}
acquireLock();
ActionListener listener = new ActionListener() {
@Override
public void onSuccess() {
status.setValue(app.getString(R.string.callback_waiting));
requestGroupInfo(1);
}
LiveData<Boolean> getIs5GhzSupported() {
return is5GhzSupported;
}
@Override
public void onFailure(int reason) {
if (reason == 2) requestGroupInfo(1); // Hotspot already running
else releaseWifiP2pHotspot(app.getString(R.string.callback_failed, reason));
}
};
try {
wifiP2pManager.createGroup(channel, listener);
} catch (SecurityException e) {
releaseWifiP2pHotspot(app.getString(R.string.callback_permission_denied));
}
@UiThread
void startWifiP2pHotspot() {
hotspotManager.startWifiP2pHotspot();
}
private void requestGroupInfo(int attempt) {
GroupInfoListener listener = group -> {
if (group == null) {
// On some devices we need to wait for the group info to become available
if (attempt < MAX_GROUP_INFO_ATTEMPTS) {
handler.postDelayed(() -> requestGroupInfo(attempt + 1), 1000);
} else {
releaseWifiP2pHotspot(app.getString(R.string.callback_no_group_info));
}
} else {
config.setValue(new NetworkConfig(group.getNetworkName(), group.getPassphrase(),
true));
status.setValue(app.getString(R.string.callback_started));
}
};
try {
wifiP2pManager.requestGroupInfo(channel, listener);
} catch (SecurityException e) {
releaseWifiP2pHotspot(app.getString(R.string.callback_permission_denied));
}
@UiThread
void stopWifiP2pHotspot() {
// stop the webserver before the hotspot
// TODO maybe off-load stopping to IoExecutor as it can block
webServerManager.stopWebServer();
hotspotManager.stopWifiP2pHotspot();
}
private void releaseWifiP2pHotspot(String statusMessage) {
if (SDK_INT >= 27) channel.close();
channel = null;
releaseLock();
config.setValue(null);
status.setValue(statusMessage);
@Override
protected void onCleared() {
stopWifiP2pHotspot();
}
void stopWifiP2pHotspot() {
if (channel == null) return;
wifiP2pManager.removeGroup(channel, new ActionListener() {
@Override
public void onStartingHotspot() {
status.setValue(new StartingHotspot());
}
@Override
public void onSuccess() {
releaseWifiP2pHotspot(app.getString(R.string.hotspot_stopped));
}
@Nullable
// Field to temporarily store the network config received via onHotspotStarted()
// in order to post it along with a HotspotStarted status
private volatile NetworkConfig networkConfig;
@Override
public void onFailure(int reason) {
releaseWifiP2pHotspot(app.getString(R.string.hotspot_stopped));
}
});
@Override
public void onHotspotStarted(NetworkConfig networkConfig) {
this.networkConfig = networkConfig;
LOG.info("starting webserver");
// TODO: offload this to the IoExecutor
webServerManager.startWebServer();
}
@Override
protected void onCleared() {
stopWifiP2pHotspot();
public void onDeviceConnected() {
Toast.makeText(getApplication(), R.string.connected_toast, LENGTH_LONG)
.show();
}
@SuppressLint("WakelockTimeout")
private void acquireLock() {
// WIFI_MODE_FULL has no effect on API >= 29
int lockType = SDK_INT >= 29 ? WIFI_MODE_FULL_HIGH_PERF : WIFI_MODE_FULL;
wifiLock = wifiManager.createWifiLock(lockType, lockTag);
wifiLock.acquire();
@Override
public void onHotspotStopped() {
status.setValue(new HotspotStopped());
LOG.info("stopping webserver");
// TODO maybe off-load stopping to IoExecutor as it can block
webServerManager.stopWebServer();
}
private void releaseLock() {
wifiLock.release();
@Override
public void onHotspotError(String error) {
status.setValue(new HotspotError(error));
// TODO maybe off-load stopping to IoExecutor as it can block
webServerManager.stopWebServer();
}
static class NetworkConfig {
final String ssid, password;
final boolean hidden;
@Override
@WorkerThread
public void onWebServerStarted(String url) {
status.postValue(new HotspotStarted(networkConfig, url));
networkConfig = null;
}
NetworkConfig(String ssid, String password, boolean hidden) {
this.ssid = ssid;
this.password = password;
this.hidden = hidden;
}
@Override
@WorkerThread
public void onWebServerError() {
status.postValue(new HotspotError(
getApplication().getString(R.string.web_server_error)));
}
}
package org.briarproject.hotspot;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.List;
import java.util.logging.Logger;
import androidx.annotation.Nullable;
import static java.util.Collections.emptyList;
import static java.util.Collections.list;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.hotspot.LogUtils.logException;
class NetworkUtils {
private final static Logger LOG = getLogger(NetworkUtils.class.getName());
@Nullable
static InetAddress getAccessPointAddress() {
for (NetworkInterface i : getNetworkInterfaces()) {
if (i.getName().startsWith("p2p")) {
for (InterfaceAddress a : i.getInterfaceAddresses()) {
if (a.getAddress().getAddress().length == 4)
return a.getAddress();
}
}
}
return null;
}
static String getNetworkInterfaceSummary() {
StringBuilder sb = new StringBuilder();
for (NetworkInterface i : getNetworkInterfaces()) {
sb.append(i.getName()).append(":");
for (InterfaceAddress a : i.getInterfaceAddresses()) {
if (a.getAddress().getAddress().length <= 4) {
sb.append(" ").append(a.getAddress());
}
}
sb.append("\n");
}
return sb.toString();
}
static List<NetworkInterface> getNetworkInterfaces() {
try {
Enumeration<NetworkInterface> ifaces =
NetworkInterface.getNetworkInterfaces();
return ifaces == null ? emptyList() : list(ifaces);
} catch (SocketException e) {
logException(LOG, WARNING, e);
return emptyList();
}
}
}
......@@ -21,9 +21,10 @@ class QrCodeUtils {
private static String TAG = QrCodeUtils.class.getName();
static String createWifiLoginString(String ssid, String password, boolean hidden) {
static String createWifiLoginString(String ssid, String password) {
// https://en.wikipedia.org/wiki/QR_code#WiFi_network_login
return "WIFI:S:" + ssid + ";P:" + password + ";T:WPA;H:" + hidden;
// do not remove the dangling ';', it can cause problems to omit it
return "WIFI:S:" + ssid + ";T:WPA;P:" + password + ";;";
}
@Nullable
......@@ -32,7 +33,8 @@ class QrCodeUtils {
int largestDimen = max(dm.widthPixels, dm.heightPixels);
int size = min(smallestDimen, largestDimen / 2);
try {
BitMatrix encoded = new QRCodeWriter().encode(input, QR_CODE, size, size);
BitMatrix encoded =
new QRCodeWriter().encode(input, QR_CODE, size, size);
return renderQrCode(encoded);
} catch (WriterException e) {
Log.w(TAG, e);
......
package org.briarproject.hotspot;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.hotspot.QrCodeUtils.createQrCode;
public class ServerFragment extends Fragment {
private MainViewModel viewModel;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
setHasOptionsMenu(true);
viewModel = new ViewModelProvider(requireActivity())
.get(MainViewModel.class);
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_server, container, false);
}
@Override
public void onViewCreated(@NonNull View v,
@Nullable Bundle savedInstanceState) {
super.onViewCreated(v, savedInstanceState);
ImageView qrCode = v.findViewById(R.id.qr_code);
TextView urlView = v.findViewById(R.id.url);
viewModel.getStatus().observe(getViewLifecycleOwner(), status -> {
if (status instanceof HotspotState.HotspotStarted) {
HotspotState.HotspotStarted state =
(HotspotState.HotspotStarted) status;
Bitmap qrCodeBitmap = createQrCode(
getResources().getDisplayMetrics(), state.getUrl());
if (qrCodeBitmap == null) {
qrCode.setVisibility(GONE);
} else {
qrCode.setImageBitmap(qrCodeBitmap);
qrCode.setVisibility(VISIBLE);
}
urlView.setText(state.getUrl());
}
});
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu,
@NonNull MenuInflater inflater) {
inflater.inflate(R.menu.main, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.interfaces) {
getParentFragmentManager().beginTransaction()
.replace(R.id.fragment_container, new InterfacesFragment())
.addToBackStack("INTERFACES")
.commit();
return true;
}
return super.onOptionsItemSelected(item);
}
}