diff --git a/app/build.gradle b/app/build.gradle
index c0e2b0b91..0d8fc2e12 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -130,6 +130,7 @@ dependencies {
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "io.objectbox:objectbox-kotlin:$objectboxVersion"
implementation "io.objectbox:objectbox-rxjava:$objectboxVersion"
+ implementation 'com.google.android.gms:play-services-location:17.0.0'
testImplementation "org.junit.jupiter:junit-jupiter:5.4.2"
testImplementation "io.mockk:mockk:1.9"
@@ -248,9 +249,9 @@ android {
'LogConditional'
warning 'UnknownNullness',
- 'SelectableText',
- 'IconDensities',
- 'SyntheticAccessor'
+ 'SelectableText',
+ 'IconDensities',
+ 'SyntheticAccessor'
baseline file("lint-baseline.xml")
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 09389d7a9..ddc607613 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,5 +1,6 @@
@@ -7,11 +8,18 @@
-
-
+
+
+
+
+
+
+
+
+
+
@@ -159,6 +168,7 @@
+
+
+
-
@@ -202,12 +213,14 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="main.MainActivity"/>
-
+
@@ -223,4 +236,5 @@
-
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/di/ServiceScope.kt b/app/src/main/java/org/kiwix/kiwixmobile/di/ServiceScope.kt
new file mode 100644
index 000000000..70f20d034
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/di/ServiceScope.kt
@@ -0,0 +1,25 @@
+/*
+ * Kiwix Android
+ * Copyright (C) 2018 Kiwix
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.kiwix.kiwixmobile.di
+
+import javax.inject.Scope
+import kotlin.annotation.AnnotationRetention.RUNTIME
+
+@Scope
+@Retention(RUNTIME)
+annotation class ServiceScope
\ No newline at end of file
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/di/components/ApplicationComponent.java b/app/src/main/java/org/kiwix/kiwixmobile/di/components/ApplicationComponent.java
index bd79ba8ea..9fb044d1e 100644
--- a/app/src/main/java/org/kiwix/kiwixmobile/di/components/ApplicationComponent.java
+++ b/app/src/main/java/org/kiwix/kiwixmobile/di/components/ApplicationComponent.java
@@ -54,6 +54,8 @@ public interface ApplicationComponent {
ActivityComponent.Builder activityComponent();
+ ServiceComponent.Builder serviceComponent();
+
void inject(KiwixApplication application);
void inject(DownloadService service);
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/di/components/ServiceComponent.kt b/app/src/main/java/org/kiwix/kiwixmobile/di/components/ServiceComponent.kt
new file mode 100644
index 000000000..d20878324
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/di/components/ServiceComponent.kt
@@ -0,0 +1,22 @@
+package org.kiwix.kiwixmobile.di.components
+
+import android.app.Service
+import dagger.BindsInstance
+import dagger.Subcomponent
+import org.kiwix.kiwixmobile.di.ServiceScope
+import org.kiwix.kiwixmobile.di.modules.ServiceModule
+import org.kiwix.kiwixmobile.wifi_hotspot.HotspotService
+
+@Subcomponent(modules = [ServiceModule::class])
+@ServiceScope
+interface ServiceComponent {
+ fun inject(hotspotService: HotspotService)
+
+ @Subcomponent.Builder
+ interface Builder {
+
+ @BindsInstance fun service(service: Service): Builder
+
+ fun build(): ServiceComponent
+ }
+}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ActivityBindingModule.java b/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ActivityBindingModule.java
index ee09602b3..36f68a287 100644
--- a/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ActivityBindingModule.java
+++ b/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ActivityBindingModule.java
@@ -17,6 +17,8 @@ import org.kiwix.kiwixmobile.main.MainModule;
import org.kiwix.kiwixmobile.search.SearchActivity;
import org.kiwix.kiwixmobile.settings.KiwixSettingsActivity;
import org.kiwix.kiwixmobile.splash.SplashActivity;
+import org.kiwix.kiwixmobile.webserver.ZimHostActivity;
+import org.kiwix.kiwixmobile.webserver.ZimHostModule;
import org.kiwix.kiwixmobile.zim_manager.ZimManageActivity;
/**
@@ -70,4 +72,8 @@ public abstract class ActivityBindingModule {
@PerActivity
@ContributesAndroidInjector
public abstract HelpActivity provideHelpActivity();
+
+ @PerActivity
+ @ContributesAndroidInjector(modules = ZimHostModule.class)
+ public abstract ZimHostActivity provideZimHostActivity();
}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/di/modules/JNIModule.java b/app/src/main/java/org/kiwix/kiwixmobile/di/modules/JNIModule.java
index 9e15c1092..07748a632 100644
--- a/app/src/main/java/org/kiwix/kiwixmobile/di/modules/JNIModule.java
+++ b/app/src/main/java/org/kiwix/kiwixmobile/di/modules/JNIModule.java
@@ -18,6 +18,7 @@
package org.kiwix.kiwixmobile.di.modules;
import android.content.Context;
+import androidx.annotation.NonNull;
import dagger.Module;
import dagger.Provides;
import javax.inject.Singleton;
@@ -30,7 +31,7 @@ import org.kiwix.kiwixlib.JNIKiwix;
@Module public class JNIModule {
@Provides
@Singleton
- public JNIKiwix providesJNIKiwix(Context context) {
+ public JNIKiwix providesJNIKiwix(@NonNull Context context) {
return new JNIKiwix(context);
}
}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ServiceModule.kt b/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ServiceModule.kt
new file mode 100644
index 000000000..91905e0b7
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ServiceModule.kt
@@ -0,0 +1,70 @@
+package org.kiwix.kiwixmobile.di.modules
+
+import android.app.Application
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Context
+import android.net.wifi.WifiManager
+import dagger.Module
+import dagger.Provides
+import org.kiwix.kiwixlib.JNIKiwixLibrary
+import org.kiwix.kiwixlib.JNIKiwixServer
+import org.kiwix.kiwixmobile.di.ServiceScope
+import org.kiwix.kiwixmobile.webserver.WebServerHelper
+import org.kiwix.kiwixmobile.wifi_hotspot.HotspotNotificationManager
+import org.kiwix.kiwixmobile.wifi_hotspot.HotspotStateListener
+import org.kiwix.kiwixmobile.wifi_hotspot.IpAddressCallbacks
+import org.kiwix.kiwixmobile.wifi_hotspot.WifiHotspotManager
+
+@Module
+class ServiceModule {
+
+ @Provides
+ @ServiceScope
+ fun providesWebServerHelper(
+ jniKiwixLibrary: JNIKiwixLibrary,
+ kiwixServer: JNIKiwixServer,
+ ipAddressCallbacks: IpAddressCallbacks
+ ): WebServerHelper = WebServerHelper(jniKiwixLibrary, kiwixServer, ipAddressCallbacks)
+
+ @Provides
+ @ServiceScope
+ fun providesWifiHotspotManager(
+ wifiManager: WifiManager,
+ hotspotStateListener: HotspotStateListener
+ ): WifiHotspotManager =
+ WifiHotspotManager(wifiManager, hotspotStateListener)
+
+ @Provides
+ @ServiceScope
+ fun providesHotspotStateListener(service: Service): HotspotStateListener =
+ service as HotspotStateListener
+
+ @Provides
+ @ServiceScope
+ fun providesIpAddressCallbacks(service: Service): IpAddressCallbacks =
+ service as IpAddressCallbacks
+
+ @Provides
+ @ServiceScope
+ fun providesJNIKiwixLibrary(): JNIKiwixLibrary = JNIKiwixLibrary()
+
+ @Provides
+ @ServiceScope
+ fun providesJNIKiwixServer(jniKiwixLibrary: JNIKiwixLibrary): JNIKiwixServer =
+ JNIKiwixServer(jniKiwixLibrary)
+
+ @Provides
+ @ServiceScope
+ fun providesWifiManager(context: Application): WifiManager =
+ context.getSystemService(Context.WIFI_SERVICE) as WifiManager
+
+ @Provides
+ @ServiceScope
+ fun providesHotspotNotificationManager(
+ notificationManager: NotificationManager,
+ context: Context
+ ): HotspotNotificationManager =
+ HotspotNotificationManager(notificationManager, context)
+}
+
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java b/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java
index 0e42baca5..347a3acdc 100644
--- a/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java
+++ b/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java
@@ -86,6 +86,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.snackbar.Snackbar;
import io.reactivex.android.schedulers.AndroidSchedulers;
+
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -111,6 +112,7 @@ import org.kiwix.kiwixmobile.utils.LanguageUtils;
import org.kiwix.kiwixmobile.utils.NetworkUtils;
import org.kiwix.kiwixmobile.utils.StyleUtils;
import org.kiwix.kiwixmobile.utils.files.FileUtils;
+import org.kiwix.kiwixmobile.webserver.ZimHostActivity;
import org.kiwix.kiwixmobile.zim_manager.ZimManageActivity;
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.StorageObserver;
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BookOnDiskDelegate;
@@ -939,6 +941,12 @@ public class MainActivity extends BaseActivity implements WebViewCallback,
Intent intentSupportKiwix = new Intent(Intent.ACTION_VIEW, uriSupportKiwix);
intentSupportKiwix.putExtra(EXTRA_EXTERNAL_LINK, true);
openExternalUrl(intentSupportKiwix);
+ break;
+
+ case R.id.menu_host_books:
+ Intent intent = new Intent(MainActivity.this, ZimHostActivity.class);
+ startActivity(intent);
+ break;
default:
break;
@@ -1687,6 +1695,7 @@ public class MainActivity extends BaseActivity implements WebViewCallback,
}
}
return;
+
default:
break;
}
@@ -1756,6 +1765,7 @@ public class MainActivity extends BaseActivity implements WebViewCallback,
menu.findItem(R.id.menu_home).setVisible(false);
menu.findItem(R.id.menu_random_article).setVisible(false);
menu.findItem(R.id.menu_searchintext).setVisible(false);
+ menu.findItem(R.id.menu_host_books).setVisible(true);
} else {
menu.findItem(R.id.menu_read_aloud).setVisible(true);
menu.findItem(R.id.menu_home).setVisible(true);
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/utils/AlertDialogShower.kt b/app/src/main/java/org/kiwix/kiwixmobile/utils/AlertDialogShower.kt
index 7f6ce0e2a..0c962abe2 100644
--- a/app/src/main/java/org/kiwix/kiwixmobile/utils/AlertDialogShower.kt
+++ b/app/src/main/java/org/kiwix/kiwixmobile/utils/AlertDialogShower.kt
@@ -3,6 +3,7 @@ package org.kiwix.kiwixmobile.utils
import android.app.Activity
import android.app.AlertDialog
import org.kiwix.kiwixmobile.R
+import org.kiwix.kiwixmobile.utils.KiwixDialog.StartHotspotManually
import javax.inject.Inject
class AlertDialogShower @Inject constructor(
@@ -27,9 +28,17 @@ class AlertDialogShower @Inject constructor(
clickListeners.getOrNull(0)
?.invoke()
}
- setNegativeButton(dialog.negativeMessage) { _, _ ->
- clickListeners.getOrNull(1)
- ?.invoke()
+ dialog.negativeMessage?.let {
+ setNegativeButton(it) { _, _ ->
+ clickListeners.getOrNull(1)
+ ?.invoke()
+ }
+ }
+ if (dialog is StartHotspotManually) {
+ setNeutralButton(dialog.neutralMessage) { _, _ ->
+ clickListeners.getOrNull(2)
+ ?.invoke()
+ }
}
}
.show()
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/utils/Constants.java b/app/src/main/java/org/kiwix/kiwixmobile/utils/Constants.java
index 5b070dec0..1245b0d8a 100644
--- a/app/src/main/java/org/kiwix/kiwixmobile/utils/Constants.java
+++ b/app/src/main/java/org/kiwix/kiwixmobile/utils/Constants.java
@@ -126,6 +126,8 @@ public final class Constants {
public static final String EXTRA_NOTIFICATION_ID = "notificationID";
+ public static final String HOTSPOT_SERVICE_CHANNEL_ID = "hotspotService";
+
public static final String EXTRA_WEBVIEWS_LIST = "webviewsList";
public static final String EXTRA_SEARCH_TEXT = "searchText";
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/utils/KiwixDialog.kt b/app/src/main/java/org/kiwix/kiwixmobile/utils/KiwixDialog.kt
index c6347b0bb..568f88435 100644
--- a/app/src/main/java/org/kiwix/kiwixmobile/utils/KiwixDialog.kt
+++ b/app/src/main/java/org/kiwix/kiwixmobile/utils/KiwixDialog.kt
@@ -1,13 +1,15 @@
package org.kiwix.kiwixmobile.utils
+import android.net.wifi.WifiConfiguration
import org.kiwix.kiwixmobile.R
+import org.kiwix.kiwixmobile.R.string
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
sealed class KiwixDialog(
val title: Int?,
val message: Int,
val positiveMessage: Int,
- val negativeMessage: Int
+ val negativeMessage: Int?
) {
data class DeleteZim(override val args: Array) : KiwixDialog(
@@ -30,23 +32,55 @@ sealed class KiwixDialog(
}
object LocationPermissionRationale : KiwixDialog(
- null, R.string.permission_rationale_location, android.R.string.yes, android.R.string.cancel
+ null,
+ R.string.permission_rationale_location,
+ android.R.string.yes,
+ android.R.string.cancel
)
object StoragePermissionRationale : KiwixDialog(
- null, R.string.request_storage, android.R.string.yes, android.R.string.cancel
+ null, R.string.request_storage, android.R.string.yes, android.R.string.cancel
)
object EnableWifiP2pServices : KiwixDialog(
- null, R.string.request_enable_wifi, R.string.yes, android.R.string.no
+ null, R.string.request_enable_wifi, R.string.yes, android.R.string.no
)
object EnableLocationServices : KiwixDialog(
- null, R.string.request_enable_location, R.string.yes, android.R.string.no
+ null, R.string.request_enable_location, R.string.yes, android.R.string.no
)
+ object TurnOffHotspotManually : KiwixDialog(
+ R.string.hotspot_failed_title,
+ R.string.hotspot_failed_message,
+ R.string.go_to_wifi_settings_label,
+ null
+ )
+
+ data class ShowHotspotDetails(override val args: Array) : KiwixDialog(
+ R.string.hotspot_turned_on,
+ R.string.hotspot_details_message,
+ android.R.string.ok,
+ null
+ ), HasBodyFormatArgs {
+ constructor(wifiConfiguration: WifiConfiguration) : this(
+ arrayOf(
+ wifiConfiguration.SSID,
+ wifiConfiguration.preSharedKey
+ )
+ )
+ }
+
+ data class StartHotspotManually(val neutralMessage: Int = string.hotspot_dialog_neutral_button) :
+ KiwixDialog(
+ string.hotspot_dialog_title,
+ string.hotspot_dialog_message,
+ string.go_to_settings_label,
+ null
+ )
+
data class FileTransferConfirmation(override val args: Array) : KiwixDialog(
- null, R.string.transfer_to, R.string.yes, android.R.string.cancel
+ null, R.string.transfer_to, R.string.yes, android.R.string.cancel
), HasBodyFormatArgs {
constructor(selectedPeerDeviceName: String) : this(arrayOf(selectedPeerDeviceName))
}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/utils/ServerUtils.java b/app/src/main/java/org/kiwix/kiwixmobile/utils/ServerUtils.java
new file mode 100644
index 000000000..c88852849
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/utils/ServerUtils.java
@@ -0,0 +1,61 @@
+package org.kiwix.kiwixmobile.utils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+
+public class ServerUtils {
+ public static int port;
+ public static boolean isServerStarted;
+ public static final String INVALID_IP = "-1";
+
+ // get Ip address of the device's wireless access point i.e. wifi hotspot OR wifi network
+ @Nullable public static String getIpAddress() {
+ String ip = "";
+ try {
+ Enumeration enumNetworkInterfaces = NetworkInterface
+ .getNetworkInterfaces();
+ while (enumNetworkInterfaces.hasMoreElements()) {
+ NetworkInterface networkInterface = enumNetworkInterfaces
+ .nextElement();
+ Enumeration enumInetAddress = networkInterface
+ .getInetAddresses();
+ while (enumInetAddress.hasMoreElements()) {
+ InetAddress inetAddress = enumInetAddress.nextElement();
+
+ if (inetAddress.isSiteLocalAddress()) {
+ ip += inetAddress.getHostAddress() + "\n";
+ }
+ }
+ }
+ //To remove extra characters from IP for Android Pie
+ if (ip.length() > 14) {
+ for (int i = 15; i < 18; i++) {
+ if ((ip.charAt(i) == '.')) {
+ ip = ip.substring(0, i - 2);
+ break;
+ }
+ }
+ }
+ } catch (SocketException e) {
+ e.printStackTrace();
+ ip += "Something Wrong! " + e.toString() + "\n";
+ }
+ return ip;
+ }
+
+ @NonNull public static String getSocketAddress() {
+ String address = "http://" + getIpAddress() + ":" + port;
+ address = address.replaceAll("\n", "");
+ return address;
+ }
+
+ @Nullable public static String getIp() {
+ String ip = getIpAddress();
+ ip = ip.replaceAll("\n", "");
+ return ip.length() == 0 ? INVALID_IP : ip;
+ }
+}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/webserver/LocationCallbacks.java b/app/src/main/java/org/kiwix/kiwixmobile/webserver/LocationCallbacks.java
new file mode 100644
index 000000000..ab461d809
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/webserver/LocationCallbacks.java
@@ -0,0 +1,5 @@
+package org.kiwix.kiwixmobile.webserver;
+
+public interface LocationCallbacks {
+ void onLocationSet();
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/webserver/LocationServicesHelper.java b/app/src/main/java/org/kiwix/kiwixmobile/webserver/LocationServicesHelper.java
new file mode 100644
index 000000000..aa2ef1b3d
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/webserver/LocationServicesHelper.java
@@ -0,0 +1,107 @@
+package org.kiwix.kiwixmobile.webserver;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.android.gms.common.api.ApiException;
+import com.google.android.gms.common.api.ResolvableApiException;
+import com.google.android.gms.location.LocationRequest;
+import com.google.android.gms.location.LocationSettingsRequest;
+import com.google.android.gms.location.LocationSettingsResponse;
+import com.google.android.gms.location.LocationSettingsStates;
+import com.google.android.gms.location.LocationSettingsStatusCodes;
+import com.google.android.gms.tasks.Task;
+import javax.inject.Inject;
+
+public class LocationServicesHelper {
+ private static final String TAG = "LocationServicesHelper";
+ private final LocationCallbacks locationCallbacks;
+ private final Activity activity;
+ private static final int LOCATION_SETTINGS_PERMISSION_RESULT = 101;
+
+ @Inject
+ public LocationServicesHelper(@NonNull Activity activity,
+ @NonNull LocationCallbacks locationCallbacks) {
+ this.activity = activity;
+ this.locationCallbacks = locationCallbacks;
+ }
+
+ private Task task;
+
+ public void setupLocationServices() {
+ LocationRequest locationRequest = new LocationRequest();
+ locationRequest.setInterval(10);
+ locationRequest.setSmallestDisplacement(10);
+ locationRequest.setFastestInterval(10);
+ locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
+ LocationSettingsRequest.Builder builder = new
+ LocationSettingsRequest.Builder();
+ builder.addLocationRequest(locationRequest);
+
+ task = com.google.android.gms.location.LocationServices.getSettingsClient(activity)
+ .checkLocationSettings(builder.build());
+
+ locationSettingsResponseBuilder();
+ }
+
+ private void locationSettingsResponseBuilder() {
+ task.addOnCompleteListener(task -> {
+ try {
+ LocationSettingsResponse response = task.getResult(ApiException.class);
+ // All location settings are satisfied. The client can initialize location
+ // requests here.
+
+ locationCallbacks.onLocationSet();
+ //}
+ } catch (ApiException exception) {
+ switch (exception.getStatusCode()) {
+ case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
+ // Location settings are not satisfied. But could be fixed by showing the
+ // user a dialog.
+ try {
+ // Cast to a resolvable exception.
+ ResolvableApiException resolvable = (ResolvableApiException) exception;
+ // Show the dialog by calling startResolutionForResult(),
+ // and check the result in onActivityResult().
+ resolvable.startResolutionForResult(
+ activity,
+ LOCATION_SETTINGS_PERMISSION_RESULT);
+ } catch (IntentSender.SendIntentException e) {
+ // Ignore the error.
+ } catch (ClassCastException e) {
+ // Ignore, should be an impossible error.
+ }
+ break;
+ case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
+ // Location settings are not satisfied. However, we have no way to fix the
+ // settings so we won't show the dialog.
+ break;
+ default:
+ break;
+ }
+ }
+ });
+ }
+
+ public void onActivityResult(int requestCode, int resultCode, @NonNull Intent data) {
+ //Checking the result code for LocationSettings resolution
+ if (requestCode == LOCATION_SETTINGS_PERMISSION_RESULT) {
+ final LocationSettingsStates states = LocationSettingsStates.fromIntent(data);
+ switch (resultCode) {
+ case Activity.RESULT_OK:
+ // All required changes were successfully made
+ Log.v(TAG, states.isLocationPresent() + "");
+ locationCallbacks.onLocationSet();
+ break;
+ case Activity.RESULT_CANCELED:
+ // The user was asked to change settings, but chose not to
+ Log.v(TAG, "Canceled");
+ break;
+ default:
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/webserver/WebServerHelper.java b/app/src/main/java/org/kiwix/kiwixmobile/webserver/WebServerHelper.java
new file mode 100644
index 000000000..24b6f42d8
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/webserver/WebServerHelper.java
@@ -0,0 +1,99 @@
+package org.kiwix.kiwixmobile.webserver;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import io.reactivex.Flowable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import org.kiwix.kiwixlib.JNIKiwixException;
+import org.kiwix.kiwixlib.JNIKiwixLibrary;
+import org.kiwix.kiwixlib.JNIKiwixServer;
+import org.kiwix.kiwixmobile.utils.ServerUtils;
+import org.kiwix.kiwixmobile.wifi_hotspot.IpAddressCallbacks;
+
+import static org.kiwix.kiwixmobile.utils.ServerUtils.INVALID_IP;
+
+/**
+ * WebServerHelper class is used to set up the suitable environment i.e. getting the
+ * ip address and port no. before starting the WebServer
+ * Created by Adeel Zafar on 18/07/2019.
+ */
+
+public class WebServerHelper {
+ private static final String TAG = "WebServerHelper";
+ private JNIKiwixLibrary kiwixLibrary;
+ private JNIKiwixServer kiwixServer;
+ private IpAddressCallbacks ipAddressCallbacks;
+ private boolean isServerStarted;
+
+ @Inject public WebServerHelper(@NonNull JNIKiwixLibrary kiwixLibrary,
+ @NonNull JNIKiwixServer kiwixServer, @NonNull IpAddressCallbacks ipAddressCallbacks) {
+ this.kiwixLibrary = kiwixLibrary;
+ this.kiwixServer = kiwixServer;
+ this.ipAddressCallbacks = ipAddressCallbacks;
+ }
+
+ public boolean startServerHelper(@NonNull ArrayList selectedBooksPath) {
+ String ip = ServerUtils.getIpAddress();
+ if (ip.length() == 0) {
+ return false;
+ } else if (startAndroidWebServer(selectedBooksPath)) {
+ return true;
+ }
+ return isServerStarted;
+ }
+
+ public void stopAndroidWebServer() {
+ if (isServerStarted) {
+ kiwixServer.stop();
+ updateServerState(false);
+ }
+ }
+
+ private boolean startAndroidWebServer(ArrayList selectedBooksPath) {
+ if (!isServerStarted) {
+ int DEFAULT_PORT = 8080;
+ ServerUtils.port = DEFAULT_PORT;
+ for (String path : selectedBooksPath) {
+ try {
+ boolean isBookAdded = kiwixLibrary.addBook(path);
+ Log.v(TAG, "isBookAdded: " + isBookAdded + path);
+ } catch (JNIKiwixException e) {
+ Log.v(TAG, "Couldn't add book " + path);
+ }
+ }
+ kiwixServer.setPort(ServerUtils.port);
+ updateServerState(kiwixServer.start());
+ Log.v(TAG, "Server status" + isServerStarted);
+ }
+ return isServerStarted;
+ }
+
+ private void updateServerState(boolean isStarted) {
+ isServerStarted = isStarted;
+ ServerUtils.isServerStarted = isStarted;
+ }
+
+ //Keeps checking if hotspot has been turned using the ip address with an interval of 1 sec
+ //If no ip is found after 15 seconds, dismisses the progress dialog
+ public void pollForValidIpAddress() {
+ Flowable.interval(1, TimeUnit.SECONDS)
+ .map(__ -> ServerUtils.getIp())
+ .filter(s -> s != INVALID_IP)
+ .timeout(15, TimeUnit.SECONDS)
+ .take(1)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ s -> {
+ ipAddressCallbacks.onIpAddressValid();
+ Log.d(TAG, "onSuccess: " + s);
+ },
+ e -> {
+ Log.d(TAG, "Unable to turn on server", e);
+ ipAddressCallbacks.onIpAddressInvalid();
+ }
+ );
+ }
+}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostActivity.java b/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostActivity.java
new file mode 100644
index 000000000..5cd9bafd5
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostActivity.java
@@ -0,0 +1,367 @@
+package org.kiwix.kiwixmobile.webserver;
+
+import android.Manifest;
+import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.net.wifi.WifiConfiguration;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import android.os.Bundle;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.recyclerview.widget.RecyclerView;
+import butterknife.BindView;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import javax.inject.Inject;
+import kotlin.Unit;
+import kotlin.jvm.functions.Function0;
+import org.kiwix.kiwixmobile.R;
+import org.kiwix.kiwixmobile.base.BaseActivity;
+import org.kiwix.kiwixmobile.utils.AlertDialogShower;
+import org.kiwix.kiwixmobile.utils.KiwixDialog;
+import org.kiwix.kiwixmobile.utils.ServerUtils;
+import org.kiwix.kiwixmobile.wifi_hotspot.HotspotService;
+import org.kiwix.kiwixmobile.zim_manager.fileselect_view.SelectionMode;
+import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BookOnDiskDelegate;
+import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskAdapter;
+import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem;
+
+import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotService.ACTION_CHECK_IP_ADDRESS;
+import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotService.ACTION_LOCATION_ACCESS_GRANTED;
+import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotService.ACTION_START_SERVER;
+import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotService.ACTION_STOP_SERVER;
+import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotService.ACTION_TOGGLE_HOTSPOT;
+
+public class ZimHostActivity extends BaseActivity implements
+ ZimHostCallbacks, ZimHostContract.View, LocationCallbacks {
+
+ @BindView(R.id.startServerButton)
+ Button startServerButton;
+ @BindView(R.id.server_textView)
+ TextView serverTextView;
+ @BindView(R.id.recycler_view_zim_host)
+ RecyclerView recyclerViewZimHost;
+
+ @Inject
+ ZimHostContract.Presenter presenter;
+
+ @Inject
+ AlertDialogShower alertDialogShower;
+
+ @Inject
+ LocationServicesHelper locationServicesHelper;
+
+ private static final String TAG = "ZimHostActivity";
+ private static final int MY_PERMISSIONS_ACCESS_FINE_LOCATION = 102;
+ private static final String IP_STATE_KEY = "ip_state_key";
+ public static final String SELECTED_ZIM_PATHS_KEY = "selected_zim_paths";
+
+ private BooksOnDiskAdapter booksAdapter;
+ private BookOnDiskDelegate.BookDelegate bookDelegate;
+ private HotspotService hotspotService;
+ private String ip;
+ private ServiceConnection serviceConnection;
+ private ProgressDialog progressDialog;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_zim_host);
+
+ setUpToolbar();
+
+ if (savedInstanceState != null) {
+ ip = savedInstanceState.getString(IP_STATE_KEY);
+ layoutServerStarted();
+ }
+ bookDelegate =
+ new BookOnDiskDelegate.BookDelegate(sharedPreferenceUtil,
+ null,
+ null,
+ bookOnDiskItem -> {
+ select(bookOnDiskItem);
+ return Unit.INSTANCE;
+ });
+ bookDelegate.setSelectionMode(SelectionMode.MULTI);
+ booksAdapter = new BooksOnDiskAdapter(bookDelegate,
+ BookOnDiskDelegate.LanguageDelegate.INSTANCE
+ );
+
+ presenter.attachView(this);
+
+ presenter.loadBooks();
+ recyclerViewZimHost.setAdapter(booksAdapter);
+
+ serviceConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ hotspotService = ((HotspotService.HotspotBinder) service).getService();
+ hotspotService.registerCallBack(ZimHostActivity.this);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+ }
+ };
+
+ startServerButton.setOnClickListener(v -> {
+ //Get the path of ZIMs user has selected
+ if (!ServerUtils.isServerStarted) {
+ if (getSelectedBooksPath().size() > 0) {
+ startHotspotHelper();
+ } else {
+ Toast.makeText(ZimHostActivity.this, R.string.no_books_selected_toast_message,
+ Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ startHotspotHelper();
+ }
+ });
+ }
+
+ private void startHotspotHelper() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ toggleHotspot();
+ } else {
+ if (ServerUtils.isServerStarted) {
+ startService(createHotspotIntent(ACTION_STOP_SERVER));
+ } else {
+ startHotspotManuallyDialog();
+ }
+ }
+ }
+
+ private ArrayList getSelectedBooksPath() {
+ ArrayList selectedBooksPath = new ArrayList<>();
+ for (BooksOnDiskListItem item : booksAdapter.getItems()) {
+ if (item.isSelected()) {
+ BooksOnDiskListItem.BookOnDisk bookOnDisk = (BooksOnDiskListItem.BookOnDisk) item;
+ File file = bookOnDisk.getFile();
+ selectedBooksPath.add(file.getAbsolutePath());
+ Log.v(TAG, "ZIM PATH : " + file.getAbsolutePath());
+ }
+ }
+ return selectedBooksPath;
+ }
+
+ private void select(@NonNull BooksOnDiskListItem.BookOnDisk bookOnDisk) {
+ ArrayList booksList = new ArrayList<>();
+ for (BooksOnDiskListItem item : booksAdapter.getItems()) {
+ if (item.equals(bookOnDisk)) {
+ item.setSelected(!item.isSelected());
+ }
+ booksList.add(item);
+ }
+ booksAdapter.setItems(booksList);
+ }
+
+ @Override protected void onStart() {
+ super.onStart();
+ bindService();
+ }
+
+ @Override protected void onStop() {
+ super.onStop();
+ unbindService();
+ }
+
+ private void bindService() {
+ bindService(new Intent(this, HotspotService.class), serviceConnection,
+ Context.BIND_AUTO_CREATE);
+ }
+
+ private void unbindService() {
+ if (hotspotService != null) {
+ unbindService(serviceConnection);
+ }
+ }
+
+ private void toggleHotspot() {
+ //Check if location permissions are granted
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED) {
+ //Toggle hotspot if location permissions are granted
+ startService(createHotspotIntent(
+ ACTION_TOGGLE_HOTSPOT));
+ } else {
+ //Ask location permission if not granted
+ ActivityCompat.requestPermissions(this,
+ new String[] { Manifest.permission.ACCESS_FINE_LOCATION },
+ MY_PERMISSIONS_ACCESS_FINE_LOCATION);
+ }
+ }
+
+ @Override protected void onResume() {
+ super.onResume();
+ presenter.loadBooks();
+ if (ServerUtils.isServerStarted) {
+ ip = ServerUtils.getSocketAddress();
+ layoutServerStarted();
+ }
+ }
+
+ private void layoutServerStarted() {
+ serverTextView.setText(getString(R.string.server_started_message, ip));
+ startServerButton.setText(getString(R.string.stop_server_label));
+ startServerButton.setBackgroundColor(getResources().getColor(R.color.stopServer));
+ bookDelegate.setSelectionMode(SelectionMode.NORMAL);
+ for (BooksOnDiskListItem item : booksAdapter.getItems()) {
+ item.setSelected(false);
+ }
+ booksAdapter.notifyDataSetChanged();
+ }
+
+ private void layoutServerStopped() {
+ serverTextView.setText(getString(R.string.server_textview_default_message));
+ startServerButton.setText(getString(R.string.start_server_label));
+ startServerButton.setBackgroundColor(getResources().getColor(R.color.greenTick));
+ bookDelegate.setSelectionMode(SelectionMode.MULTI);
+ booksAdapter.notifyDataSetChanged();
+ }
+
+ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ if (requestCode == MY_PERMISSIONS_ACCESS_FINE_LOCATION) {
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ toggleHotspot();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ locationServicesHelper.onActivityResult(requestCode, resultCode, (data));
+ }
+
+ @Override protected void onDestroy() {
+ super.onDestroy();
+ presenter.detachView();
+ }
+
+ private void setUpToolbar() {
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setTitle(getString(R.string.menu_host_books));
+ getSupportActionBar().setHomeButtonEnabled(true);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ toolbar.setNavigationOnClickListener(v -> onBackPressed());
+ }
+
+ //Advice user to turn on hotspot manually for API<26
+ private void startHotspotManuallyDialog() {
+
+ alertDialogShower.show(new KiwixDialog.StartHotspotManually(),
+ () -> {
+ launchTetheringSettingsScreen();
+ return Unit.INSTANCE;
+ },
+ null,
+ () -> {
+ progressDialog =
+ ProgressDialog.show(this,
+ getString(R.string.progress_dialog_starting_server), "",
+ true);
+ startService(createHotspotIntent(ACTION_CHECK_IP_ADDRESS));
+ return Unit.INSTANCE;
+ }
+ );
+ }
+
+ private Intent createHotspotIntent(String action) {
+ return new Intent(this, HotspotService.class).setAction(action);
+ }
+
+ @Override public void onServerStarted(@NonNull String ipAddress) {
+ this.ip = ipAddress;
+ layoutServerStarted();
+ }
+
+ @Override public void onServerStopped() {
+ layoutServerStopped();
+ }
+
+ @Override public void onServerFailedToStart() {
+ Toast.makeText(this, R.string.server_failed_toast_message, Toast.LENGTH_LONG).show();
+ }
+
+ @Override public void onHotspotTurnedOn(@NonNull WifiConfiguration wifiConfiguration) {
+ alertDialogShower.show(new KiwixDialog.ShowHotspotDetails(wifiConfiguration),
+ (Function0) () -> {
+ progressDialog =
+ ProgressDialog.show(this,
+ getString(R.string.progress_dialog_starting_server), "",
+ true);
+ startService(createHotspotIntent(ACTION_CHECK_IP_ADDRESS));
+ return Unit.INSTANCE;
+ });
+ }
+
+ private void launchTetheringSettingsScreen() {
+ final Intent intent = new Intent(Intent.ACTION_MAIN, null);
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ final ComponentName cn =
+ new ComponentName("com.android.settings", "com.android.settings.TetherSettings");
+ intent.setComponent(cn);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+
+ @Override public void onHotspotFailedToStart() {
+ //Show a dialog to turn off default hotspot
+ alertDialogShower.show(KiwixDialog.TurnOffHotspotManually.INSTANCE,
+ (Function0) () -> {
+ launchTetheringSettingsScreen();
+ return Unit.INSTANCE;
+ });
+ }
+
+ @Override public void requestLocationAccess() {
+ locationServicesHelper.setupLocationServices();
+ }
+
+ @Override protected void onSaveInstanceState(@Nullable Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (ServerUtils.isServerStarted) {
+ outState.putString(IP_STATE_KEY, ip);
+ }
+ }
+
+ @Override public void addBooks(@Nullable List books) {
+ booksAdapter.setItems(books);
+ }
+
+ @Override public void onLocationSet() {
+ startService(createHotspotIntent(ACTION_LOCATION_ACCESS_GRANTED));
+ }
+
+ @Override public void onIpAddressValid() {
+ progressDialog.dismiss();
+ startService(createHotspotIntent(ACTION_START_SERVER).putStringArrayListExtra(
+ SELECTED_ZIM_PATHS_KEY, getSelectedBooksPath()));
+ }
+
+ @Override public void onIpAddressInvalid() {
+ progressDialog.dismiss();
+ Toast.makeText(this, R.string.server_failed_message,
+ Toast.LENGTH_SHORT)
+ .show();
+ }
+}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostCallbacks.java b/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostCallbacks.java
new file mode 100644
index 000000000..1d3934ac6
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostCallbacks.java
@@ -0,0 +1,23 @@
+package org.kiwix.kiwixmobile.webserver;
+
+import android.net.wifi.WifiConfiguration;
+import androidx.annotation.NonNull;
+
+public interface ZimHostCallbacks {
+
+ void onServerStarted(@NonNull String ip);
+
+ void onServerStopped();
+
+ void onServerFailedToStart();
+
+ void onHotspotTurnedOn(@NonNull WifiConfiguration wifiConfiguration);
+
+ void onHotspotFailedToStart();
+
+ void requestLocationAccess();
+
+ void onIpAddressValid();
+
+ void onIpAddressInvalid();
+}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostContract.java b/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostContract.java
new file mode 100644
index 000000000..a20cb7b5d
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostContract.java
@@ -0,0 +1,18 @@
+package org.kiwix.kiwixmobile.webserver;
+
+import java.util.List;
+import org.kiwix.kiwixmobile.base.BaseContract;
+import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem;
+
+class ZimHostContract {
+
+ interface View
+ extends BaseContract.View {
+ void addBooks(List books);
+ }
+
+ interface Presenter
+ extends BaseContract.Presenter {
+ void loadBooks();
+ }
+}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostModule.java b/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostModule.java
new file mode 100644
index 000000000..45b71b4d5
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostModule.java
@@ -0,0 +1,33 @@
+package org.kiwix.kiwixmobile.webserver;
+
+import android.app.Activity;
+import dagger.Module;
+import dagger.Provides;
+import org.kiwix.kiwixmobile.di.PerActivity;
+
+@Module
+public class ZimHostModule {
+
+ @PerActivity
+ @Provides
+ ZimHostContract.Presenter provideZimHostPresenter(ZimHostPresenter zimHostPresenter) {
+ return zimHostPresenter;
+ }
+
+ @PerActivity
+ @Provides Activity providesActivity(ZimHostActivity zimHostActivity) {
+ return zimHostActivity;
+ }
+
+ @PerActivity
+ @Provides LocationServicesHelper providesLocationServicesHelper(ZimHostActivity activity,
+ LocationCallbacks locationCallbacks) {
+ return new LocationServicesHelper(activity, locationCallbacks);
+ }
+
+ @PerActivity
+ @Provides LocationCallbacks providesLocationCallbacks(ZimHostActivity activity) {
+ return activity;
+ }
+}
+
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostPresenter.java b/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostPresenter.java
new file mode 100644
index 000000000..56b62ae79
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostPresenter.java
@@ -0,0 +1,44 @@
+package org.kiwix.kiwixmobile.webserver;
+
+import android.util.Log;
+import io.reactivex.SingleObserver;
+import io.reactivex.disposables.Disposable;
+import java.util.List;
+import javax.inject.Inject;
+import org.kiwix.kiwixmobile.base.BasePresenter;
+import org.kiwix.kiwixmobile.data.DataSource;
+import org.kiwix.kiwixmobile.di.PerActivity;
+import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem;
+
+@PerActivity
+class ZimHostPresenter extends BasePresenter
+ implements ZimHostContract.Presenter {
+
+ private static final String TAG = "ZimHostPresenter";
+ private final DataSource dataSource;
+
+ @Inject ZimHostPresenter(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ @Override
+ public void loadBooks() {
+ dataSource.getLanguageCategorizedBooks()
+ .subscribe(new SingleObserver>() {
+ @Override
+ public void onSubscribe(Disposable d) {
+ compositeDisposable.add(d);
+ }
+
+ @Override
+ public void onSuccess(List books) {
+ view.addBooks(books);
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ Log.e(TAG, "Unable to load books", e);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/HotspotNotificationManager.java b/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/HotspotNotificationManager.java
new file mode 100644
index 000000000..0fc7e5cc5
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/HotspotNotificationManager.java
@@ -0,0 +1,74 @@
+package org.kiwix.kiwixmobile.wifi_hotspot;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.core.app.NotificationCompat;
+import javax.inject.Inject;
+import org.kiwix.kiwixmobile.R;
+import org.kiwix.kiwixmobile.utils.Constants;
+import org.kiwix.kiwixmobile.webserver.ZimHostActivity;
+
+import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotService.ACTION_STOP;
+
+public class HotspotNotificationManager {
+
+ public static final int HOTSPOT_NOTIFICATION_ID = 666;
+ private Context context;
+
+ @Inject
+ NotificationManager notificationManager;
+
+ @Inject
+ public HotspotNotificationManager(@NonNull NotificationManager notificationManager,
+ @NonNull Context context) {
+ this.notificationManager = notificationManager;
+ this.context = context;
+ }
+
+ private void hotspotNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel hotspotServiceChannel = new NotificationChannel(
+ Constants.HOTSPOT_SERVICE_CHANNEL_ID,
+ context.getString(R.string.hotspot_service_channel_name),
+ NotificationManager.IMPORTANCE_DEFAULT);
+ hotspotServiceChannel.setDescription(context.getString(R.string.hotspot_channel_description));
+ hotspotServiceChannel.setSound(null, null);
+ notificationManager.createNotificationChannel(hotspotServiceChannel);
+ }
+ }
+
+ @NonNull public Notification buildForegroundNotification() {
+ Intent targetIntent = new Intent(context, ZimHostActivity.class);
+ targetIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ PendingIntent contentIntent =
+ PendingIntent.getActivity(context, 0, targetIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ hotspotNotificationChannel();
+
+ Intent stopIntent = new Intent(context, HotspotService.class).setAction(ACTION_STOP);
+ PendingIntent stopHotspot =
+ PendingIntent.getService(context, 0, stopIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ return new NotificationCompat.Builder(context)
+ .setContentTitle(context.getString(R.string.hotspot_notification_content_title))
+ .setContentText(context.getString(R.string.hotspot_running))
+ .setContentIntent(contentIntent)
+ .setSmallIcon(R.mipmap.kiwix_icon)
+ .setWhen(System.currentTimeMillis())
+ .addAction(R.drawable.ic_close_white_24dp,
+ context.getString(R.string.stop_hotspot_button),
+ stopHotspot)
+ .setChannelId(Constants.HOTSPOT_SERVICE_CHANNEL_ID)
+ .build();
+ }
+
+ public void dismissNotification() {
+ notificationManager.cancel(HOTSPOT_NOTIFICATION_ID);
+ }
+}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/HotspotService.java b/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/HotspotService.java
new file mode 100644
index 000000000..1bff81792
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/HotspotService.java
@@ -0,0 +1,171 @@
+package org.kiwix.kiwixmobile.wifi_hotspot;
+
+import android.app.Service;
+import android.content.Intent;
+import android.net.wifi.WifiConfiguration;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import javax.inject.Inject;
+import org.kiwix.kiwixmobile.KiwixApplication;
+import org.kiwix.kiwixmobile.R;
+import org.kiwix.kiwixmobile.utils.ServerUtils;
+import org.kiwix.kiwixmobile.webserver.ZimHostCallbacks;
+import org.kiwix.kiwixmobile.webserver.WebServerHelper;
+
+import static org.kiwix.kiwixmobile.webserver.ZimHostActivity.SELECTED_ZIM_PATHS_KEY;
+import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotNotificationManager.HOTSPOT_NOTIFICATION_ID;
+
+/**
+ * HotspotService is used to add a foreground service for the wifi hotspot.
+ * Created by Adeel Zafar on 07/01/2019.
+ */
+
+public class HotspotService extends Service implements HotspotStateListener, IpAddressCallbacks {
+
+ public static final String ACTION_TOGGLE_HOTSPOT = "toggle_hotspot";
+ public static final String ACTION_LOCATION_ACCESS_GRANTED = "location_access_granted";
+ public static final String ACTION_START_SERVER = "start_server";
+ public static final String ACTION_STOP_SERVER = "stop_server";
+ public static final String ACTION_CHECK_IP_ADDRESS = "check_ip_address";
+
+ public static final String ACTION_STOP = "hotspot_stop";
+ private ZimHostCallbacks zimHostCallbacks;
+ private final IBinder serviceBinder = new HotspotBinder();
+
+ @Inject
+ WebServerHelper webServerHelper;
+
+ @Inject
+ WifiHotspotManager hotspotManager;
+
+ @Inject
+ HotspotNotificationManager hotspotNotificationManager;
+
+ @Override public void onCreate() {
+ KiwixApplication.getApplicationComponent()
+ .serviceComponent()
+ .service(this)
+ .build()
+ .inject(this);
+ super.onCreate();
+ }
+
+ @Override public int onStartCommand(@NonNull Intent intent, int flags, int startId) {
+ switch (intent.getAction()) {
+
+ case ACTION_TOGGLE_HOTSPOT:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (hotspotManager.isHotspotStarted()) {
+ stopHotspotAndDismissNotification();
+ } else {
+ zimHostCallbacks.requestLocationAccess();
+ }
+ }
+ break;
+
+ case ACTION_LOCATION_ACCESS_GRANTED:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ hotspotManager.turnOnHotspot();
+ }
+ break;
+
+ case ACTION_START_SERVER:
+ if (webServerHelper.startServerHelper(
+ intent.getStringArrayListExtra(SELECTED_ZIM_PATHS_KEY))) {
+ zimHostCallbacks.onServerStarted(ServerUtils.getSocketAddress());
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ startForegroundNotificationHelper();
+ }
+ Toast.makeText(this, R.string.server_started__successfully_toast_message,
+ Toast.LENGTH_SHORT).show();
+ } else {
+ zimHostCallbacks.onServerFailedToStart();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ stopForeground(true);
+ stopSelf();
+ hotspotNotificationManager.dismissNotification();
+ }
+ }
+
+ break;
+
+ case ACTION_STOP_SERVER:
+ stopHotspotAndDismissNotification();
+ break;
+
+ case ACTION_CHECK_IP_ADDRESS:
+ webServerHelper.pollForValidIpAddress();
+ break;
+
+ case ACTION_STOP:
+ stopHotspotAndDismissNotification();
+ break;
+
+ default:
+ break;
+ }
+ return START_NOT_STICKY;
+ }
+
+ @Nullable @Override public IBinder onBind(@Nullable Intent intent) {
+ return serviceBinder;
+ }
+
+ //Dismiss notification and turn off hotspot for devices>=O
+ private void stopHotspotAndDismissNotification() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ hotspotManager.turnOffHotspot();
+ } else {
+ webServerHelper.stopAndroidWebServer();
+ zimHostCallbacks.onServerStopped();
+ stopForeground(true);
+ stopSelf();
+ hotspotNotificationManager.dismissNotification();
+ }
+ }
+
+ public void registerCallBack(@Nullable ZimHostCallbacks myCallback) {
+ zimHostCallbacks = myCallback;
+ }
+
+ private void startForegroundNotificationHelper() {
+ startForeground(HOTSPOT_NOTIFICATION_ID,
+ hotspotNotificationManager.buildForegroundNotification());
+ }
+
+ @Override public void onHotspotTurnedOn(@NonNull WifiConfiguration wifiConfiguration) {
+ startForegroundNotificationHelper();
+ zimHostCallbacks.onHotspotTurnedOn(wifiConfiguration);
+ }
+
+ @Override public void onHotspotFailedToStart() {
+ zimHostCallbacks.onHotspotFailedToStart();
+ }
+
+ @Override public void onHotspotStopped() {
+ webServerHelper.stopAndroidWebServer();
+ zimHostCallbacks.onServerStopped();
+ stopForeground(true);
+ stopSelf();
+ hotspotNotificationManager.dismissNotification();
+ }
+
+ @Override public void onIpAddressValid() {
+ zimHostCallbacks.onIpAddressValid();
+ }
+
+ @Override public void onIpAddressInvalid() {
+ zimHostCallbacks.onIpAddressInvalid();
+ }
+
+ public class HotspotBinder extends Binder {
+
+ @NonNull public HotspotService getService() {
+ return HotspotService.this;
+ }
+ }
+}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/HotspotStateListener.java b/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/HotspotStateListener.java
new file mode 100644
index 000000000..9c232fae1
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/HotspotStateListener.java
@@ -0,0 +1,12 @@
+package org.kiwix.kiwixmobile.wifi_hotspot;
+
+import android.net.wifi.WifiConfiguration;
+import androidx.annotation.NonNull;
+
+public interface HotspotStateListener {
+ void onHotspotTurnedOn(@NonNull WifiConfiguration wifiConfiguration);
+
+ void onHotspotFailedToStart();
+
+ void onHotspotStopped();
+}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/IpAddressCallbacks.java b/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/IpAddressCallbacks.java
new file mode 100644
index 000000000..e3da8a063
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/IpAddressCallbacks.java
@@ -0,0 +1,8 @@
+package org.kiwix.kiwixmobile.wifi_hotspot;
+
+public interface IpAddressCallbacks {
+
+ void onIpAddressValid();
+
+ void onIpAddressInvalid();
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/WifiHotspotManager.java b/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/WifiHotspotManager.java
new file mode 100644
index 000000000..335a4711f
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/wifi_hotspot/WifiHotspotManager.java
@@ -0,0 +1,86 @@
+package org.kiwix.kiwixmobile.wifi_hotspot;
+
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.os.Handler;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import javax.inject.Inject;
+
+/**
+ * WifiHotstopManager class makes use of the Android's WifiManager and WifiConfiguration class
+ * to implement the wifi hotspot feature.
+ * Created by Adeel Zafar on 28/5/2019.
+ */
+
+public class WifiHotspotManager {
+ private static final String TAG = "WifiHotspotManager";
+ private WifiManager wifiManager;
+ private WifiManager.LocalOnlyHotspotReservation hotspotReservation;
+ private HotspotStateListener hotspotStateListener;
+
+ @Inject
+ public WifiHotspotManager(@NonNull WifiManager wifiManager,
+ @NonNull HotspotStateListener hotspotStateListener) {
+ this.wifiManager = wifiManager;
+ this.hotspotStateListener = hotspotStateListener;
+ }
+
+ //Workaround to turn on hotspot for Oreo versions
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public void turnOnHotspot() {
+ wifiManager.startLocalOnlyHotspot(new WifiManager.LocalOnlyHotspotCallback() {
+
+ @Override
+ public void onStarted(WifiManager.LocalOnlyHotspotReservation reservation) {
+ super.onStarted(reservation);
+ hotspotReservation = reservation;
+ WifiConfiguration currentConfig = hotspotReservation.getWifiConfiguration();
+
+ printCurrentConfig(currentConfig);
+ hotspotStateListener.onHotspotTurnedOn(currentConfig);
+ Log.v(TAG, "Local Hotspot Started");
+ }
+
+ @Override
+ public void onStopped() {
+ super.onStopped();
+ hotspotStateListener.onHotspotStopped();
+ Log.v(TAG, "Local Hotspot Stopped");
+ }
+
+ @Override
+ public void onFailed(int reason) {
+ super.onFailed(reason);
+ hotspotStateListener.onHotspotFailedToStart();
+ Log.v(TAG, "Local Hotspot failed to start");
+ }
+ }, new Handler());
+ }
+
+ //Workaround to turn off hotspot for Oreo versions
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public void turnOffHotspot() {
+ if (hotspotReservation != null) {
+ hotspotReservation.close();
+ hotspotReservation = null;
+ hotspotStateListener.onHotspotStopped();
+ Log.v(TAG, "Turned off hotspot");
+ }
+ }
+
+ //This method checks the state of the hostpot for devices>=Oreo
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public boolean isHotspotStarted() {
+ return hotspotReservation != null;
+ }
+
+ private void printCurrentConfig(WifiConfiguration wifiConfiguration) {
+ Log.v(TAG, "THE PASSWORD IS: "
+ + wifiConfiguration.preSharedKey
+ + " \n SSID is : "
+ + wifiConfiguration.SSID);
+ }
+}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/adapter/BookOnDiskDelegate.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/adapter/BookOnDiskDelegate.kt
index ecf2f81f2..3e97806ec 100644
--- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/adapter/BookOnDiskDelegate.kt
+++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/adapter/BookOnDiskDelegate.kt
@@ -34,7 +34,7 @@ sealed class BookOnDiskDelegate Unit,
+ private val clickAction: ((BookOnDisk) -> Unit)? = null,
private val longClickAction: ((BookOnDisk) -> Unit)? = null,
private val multiSelectAction: ((BookOnDisk) -> Unit)? = null
) : BookOnDiskDelegate() {
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/adapter/BooksOnDiskViewHolder.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/adapter/BooksOnDiskViewHolder.kt
index 8ea425e20..479b7c932 100644
--- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/adapter/BooksOnDiskViewHolder.kt
+++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/adapter/BooksOnDiskViewHolder.kt
@@ -32,7 +32,7 @@ sealed class BookOnDiskViewHolder(containerView: Vie
class BookViewHolder(
containerView: View,
private val sharedPreferenceUtil: SharedPreferenceUtil,
- private val clickAction: (BookOnDisk) -> Unit,
+ private val clickAction: ((BookOnDisk) -> Unit)?,
private val longClickAction: ((BookOnDisk) -> Unit)?,
private val multiSelectAction: ((BookOnDisk) -> Unit)?
) : BookOnDiskViewHolder(containerView) {
@@ -80,7 +80,7 @@ sealed class BookOnDiskViewHolder(containerView: Vie
}
NORMAL -> {
itemBookCheckbox.visibility = View.GONE
- item_book_clickable_area.setOnClickListener { clickAction.invoke(item) }
+ item_book_clickable_area.setOnClickListener { clickAction?.invoke(item) }
item_book_clickable_area.setOnLongClickListener {
longClickAction?.invoke(item)
return@setOnLongClickListener true
diff --git a/app/src/main/res/layout/activity_zim_host.xml b/app/src/main/res/layout/activity_zim_host.xml
new file mode 100644
index 000000000..e9ee131c6
--- /dev/null
+++ b/app/src/main/res/layout/activity_zim_host.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index 147f3bcaf..28a2123c9 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -79,6 +79,11 @@
android:visible="false"
app:showAsAction="never"/>
+
+
- #4285F4
#000000
#4CAF50
+ #E53935
#000000
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d73846c84..fe3488a2c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -14,6 +14,7 @@
Read aloud
Stop reading aloud
Support Kiwix
+ Host Books
Save Media
An error occurred when trying to save the media!
Saved media as %s to Android/media/org.kiwix…/
@@ -21,6 +22,29 @@
Search
Select a Content File (*.zim)
Open link in new tab?
+ Hotspot Service Channel
+ Failed to start hotspot
+ It seems like your hotspot is already turned on. Please disable your wifi hotspot to continue.
+ Go to WIFI settings
+ Go to settings
+ Running Hotspot
+ STOP
+ Please select books first
+ Couldn’t start server. Please turn on your hotspot
+ Couldn’t start server.
+ Server started successfully.
+ Hotspot turned on
+ Following are the details of your local hotspot. \nSSID : %1$s \nPass : %2$s
+ Select the files you wish to host on the server
+ Starting server
+ Turn on your WIFI hotspot
+ In order for this feature to work you need to first turn on your WIFI hotspot manually.
+ YES, I’VE TURNED IT ON
+ Updates about the state of your hotspot/server.
+ Kiwix Hotspot
+ Start server
+ Stop server
+ Enter this ip address into your browser to access the server %s
Error: The selected ZIM file could not be found.
Error: The selected file is not a valid ZIM file.
Error: Loading article (Url: %1$s) failed.