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 @@ + + + + + + + + + + + +