diff --git a/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/601c401a-52e1-4c2b-bc1e-071b9f93129c.vsidx b/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/601c401a-52e1-4c2b-bc1e-071b9f93129c.vsidx new file mode 100644 index 00000000..846621b7 Binary files /dev/null and b/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/601c401a-52e1-4c2b-bc1e-071b9f93129c.vsidx differ diff --git a/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/8ca1029b-05a8-42c6-b4ef-49aebba64b6d.vsidx b/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/8ca1029b-05a8-42c6-b4ef-49aebba64b6d.vsidx new file mode 100644 index 00000000..74e5b9ff Binary files /dev/null and b/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/8ca1029b-05a8-42c6-b4ef-49aebba64b6d.vsidx differ diff --git a/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/d38820d0-b6e1-466b-ba94-243cd8e48477.vsidx b/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/d38820d0-b6e1-466b-ba94-243cd8e48477.vsidx new file mode 100644 index 00000000..ce99103e Binary files /dev/null and b/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/d38820d0-b6e1-466b-ba94-243cd8e48477.vsidx differ diff --git a/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/fd60842a-bf43-450d-9a8d-24655a8973ab.vsidx b/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/fd60842a-bf43-450d-9a8d-24655a8973ab.vsidx new file mode 100644 index 00000000..0219152d Binary files /dev/null and b/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/fd60842a-bf43-450d-9a8d-24655a8973ab.vsidx differ diff --git a/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/read.lock b/.vs/Mobile-UXSDK-Beta-Android/FileContentIndex/read.lock new file mode 100644 index 00000000..e69de29b diff --git a/.vs/Mobile-UXSDK-Beta-Android/v17/.wsuo b/.vs/Mobile-UXSDK-Beta-Android/v17/.wsuo new file mode 100644 index 00000000..9a23aa21 Binary files /dev/null and b/.vs/Mobile-UXSDK-Beta-Android/v17/.wsuo differ diff --git a/.vs/Mobile-UXSDK-Beta-Android/v17/workspaceFileList.bin b/.vs/Mobile-UXSDK-Beta-Android/v17/workspaceFileList.bin new file mode 100644 index 00000000..f9dd5f00 Binary files /dev/null and b/.vs/Mobile-UXSDK-Beta-Android/v17/workspaceFileList.bin differ diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 00000000..f8b48885 --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": null +} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 00000000..6b611411 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,6 @@ +{ + "ExpandedNodes": [ + "" + ], + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 00000000..dc36aaea Binary files /dev/null and b/.vs/slnx.sqlite differ diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/beacon/BeaconWidget.kt b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/beacon/BeaconWidget.kt new file mode 100644 index 00000000..93ec3986 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/beacon/BeaconWidget.kt @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2018-2021 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.beacon + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.View +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.core.content.res.use +import dji.ux.beta.accessory.R +import dji.thirdparty.io.reactivex.functions.Action +import dji.ux.beta.accessory.widget.beacon.BeaconWidget.* +import dji.ux.beta.accessory.widget.beacon.BeaconWidgetModel.* +import dji.ux.beta.accessory.widget.beacon.BeaconWidgetModel.BeaconState.* +import dji.ux.beta.core.base.DJISDKModel +import dji.ux.beta.core.base.SchedulerProvider +import dji.ux.beta.core.base.widget.IconButtonWidget +import dji.ux.beta.core.communication.ObservableInMemoryKeyedStore +import dji.ux.beta.core.extension.* + +private const val TAG = "BeaconWidget" + +/** + * Beacon Widget + * + * This widget represents the state of the beacon accessory. + * The widget is configured to be visible only when the accessory is connected + * Tapping on the widget will toggle the beacon ON/OFF + */ +open class BeaconWidget @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : IconButtonWidget(context, attrs, defStyleAttr) { + + //region Fields + private val widgetModel: BeaconWidgetModel by lazy { + BeaconWidgetModel( + DJISDKModel.getInstance(), + ObservableInMemoryKeyedStore.getInstance() + ) + } + + /** + * Beacon inactive (off) icon + */ + var beaconInactiveIcon: Drawable? = getDrawable(R.drawable.uxsdk_ic_beacon_inactive) + set(value) { + field = value + checkAndUpdateIconColor() + } + + /** + * Beacon active (on) icon + */ + var beaconActiveIcon: Drawable? = getDrawable(R.drawable.uxsdk_ic_beacon_active) + set(value) { + field = value + checkAndUpdateIconColor() + } + + /** + * Beacon inactive (off) icon tint color + */ + @ColorInt + var beaconInactiveIconTintColor: Int? = INVALID_COLOR + set(value) { + field = value + checkAndUpdateIconColor() + } + + /** + * Beacon active (on) icon tint color + */ + @ColorInt + var beaconActiveIconTintColor: Int? = INVALID_COLOR + set(value) { + field = value + checkAndUpdateIconColor() + } + //endregion + + //region Lifecycle + init { + background = background ?: getDrawable(R.drawable.uxsdk_background_black_rectangle) + attrs?.let { initAttributes(context, it) } + } + + override fun reactToModelChanges() { + addReaction(widgetModel.beaconState + .observeOn(SchedulerProvider.ui()) + .subscribe { updateUI(it) }) + addReaction(widgetModel.productConnection + .observeOn(SchedulerProvider.ui()) + .subscribe { widgetStateDataProcessor.onNext(ModelState.ProductConnected(it)) }) + } + + override fun onClick(view: View?) { + super.onClick(view) + if (isEnabled) { + addDisposable(widgetModel.toggleBeaconState() + .observeOn(SchedulerProvider.ui()) + .subscribe(Action {}, logErrorConsumer(TAG, "toggleBeacon: ")) + ) + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + if (!isInEditMode) { + widgetModel.setup() + } + } + + override fun onDetachedFromWindow() { + if (!isInEditMode) { + widgetModel.cleanup() + } + super.onDetachedFromWindow() + } + + override fun checkAndUpdateIconColor() { + if (!isInEditMode) { + addDisposable(widgetModel.beaconState.firstOrError() + .observeOn(SchedulerProvider.ui()) + .subscribe({ updateUI(it) }, { logErrorConsumer(TAG, "Update UI ") })) + } + } + //endregion + + //region private methods + private fun updateUI(beaconState: BeaconState) { + widgetStateDataProcessor.onNext(ModelState.BeaconStateUpdated(beaconState)) + when (beaconState) { + ProductDisconnected, NotSupported -> { + isEnabled = false + foregroundImageView.setImageDrawable(beaconInactiveIcon) + foregroundImageView.updateColorFilter(getDisconnectedStateIconColor()) + hide() + } + Inactive -> setUI(beaconInactiveIcon, beaconInactiveIconTintColor) + Active -> setUI(beaconActiveIcon, beaconActiveIconTintColor) + } + } + + private fun setUI(icon: Drawable?, iconColor: Int?) { + show() + isEnabled = true + foregroundImageView.setImageDrawable(icon) + foregroundImageView.updateColorFilter(iconColor) + } + + private fun initAttributes(context: Context, attrs: AttributeSet) { + context.obtainStyledAttributes(attrs, R.styleable.BeaconWidget).use { typedArray -> + typedArray.getDrawableAndUse(R.styleable.BeaconWidget_uxsdk_beaconInactiveIcon) { + beaconInactiveIcon = it + } + typedArray.getDrawableAndUse(R.styleable.BeaconWidget_uxsdk_beaconActiveIcon) { + beaconActiveIcon = it + } + typedArray.getColorAndUse(R.styleable.BeaconWidget_uxsdk_beaconInactiveIconColor) { + beaconInactiveIconTintColor = it + } + typedArray.getColorAndUse(R.styleable.BeaconWidget_uxsdk_beaconActiveIconColor) { + beaconActiveIconTintColor = it + } + } + } + //endregion + + //region customization methods + override fun getIdealDimensionRatioString(): String { + return resources.getString(R.string.uxsdk_widget_default_ratio) + } + + /** + * Set beacon inactive icon + * + * @param resourceId to be used + */ + fun setBeaconInactiveIcon(@DrawableRes resourceId: Int) { + beaconInactiveIcon = getDrawable(resourceId) + } + + /** + * Set beacon active icon + * + * @param resourceId to be used + */ + fun setBeaconActiveIcon(@DrawableRes resourceId: Int) { + beaconActiveIcon = getDrawable(resourceId) + } + + //endregion + + //region Hooks + /** + * Class defines the widget state updates + */ + sealed class ModelState { + /** + * Product connection update + */ + data class ProductConnected(val isConnected: Boolean) : ModelState() + + /** + * Beacon state updated + */ + data class BeaconStateUpdated(val beaconState: BeaconState) : ModelState() + } + //endregion +} \ No newline at end of file diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/beacon/BeaconWidgetModel.kt b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/beacon/BeaconWidgetModel.kt new file mode 100644 index 00000000..bb9bdcdb --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/beacon/BeaconWidgetModel.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2018-2021 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.beacon + +import dji.keysdk.AccessoryAggregationKey +import dji.keysdk.DJIKey +import dji.thirdparty.io.reactivex.Completable +import dji.thirdparty.io.reactivex.Flowable +import dji.ux.beta.accessory.widget.beacon.BeaconWidgetModel.BeaconState.* +import dji.ux.beta.core.base.DJISDKModel +import dji.ux.beta.core.base.WidgetModel +import dji.ux.beta.core.communication.ObservableInMemoryKeyedStore +import dji.ux.beta.core.util.DataProcessor + +private const val TAG = "BeaconWidgetModel" + +/** + * Beacon Widget Model + * + * Widget Model for the [BeaconWidget] used to define the + * underlying logic and communication + */ +class BeaconWidgetModel( + djiSdkModel: DJISDKModel, + val keyedStore: ObservableInMemoryKeyedStore +) : WidgetModel(djiSdkModel, keyedStore) { + + //region Fields + private val isBeaconConnectedProcessor: DataProcessor = DataProcessor.create(false) + private val isBeaconEnabledProcessor: DataProcessor = DataProcessor.create(false) + private val beaconStateProcessor: DataProcessor = DataProcessor.create(ProductDisconnected) + private lateinit var beaconEnabledKey: DJIKey + + /** + * Beacon state + */ + val beaconState: Flowable + get() = beaconStateProcessor.toFlowable() + //endregion + + //region Lifecycle + override fun inSetup() { + val beaconConnectedKey: DJIKey = AccessoryAggregationKey.createBeaconKey(AccessoryAggregationKey.CONNECTION) + bindDataProcessor(beaconConnectedKey, isBeaconConnectedProcessor) + beaconEnabledKey = AccessoryAggregationKey.createBeaconKey(AccessoryAggregationKey.BEACON_ENABLED) + bindDataProcessor(beaconEnabledKey, isBeaconEnabledProcessor) + } + + override fun inCleanup() { + // Nothing to clean up + } + + override fun updateStates() { + if (productConnectionProcessor.value) { + if (isBeaconConnectedProcessor.value) { + if (isBeaconEnabledProcessor.value) { + beaconStateProcessor.onNext(Active) + } else { + beaconStateProcessor.onNext(Inactive) + } + } else { + beaconStateProcessor.onNext(NotSupported) + } + } else { + beaconStateProcessor.onNext(ProductDisconnected) + } + } + //endregion + + //region Actions + /** + * Toggle the beacon state between on and off + * + * @return Completable representing the success/failure of the action + */ + fun toggleBeaconState(): Completable { + return djiSdkModel.setValue(beaconEnabledKey, !isBeaconEnabledProcessor.value) + } + //endregion + + /** + * Class defines the states of the beacon + */ + sealed class BeaconState { + /** + * Product is disconnected + */ + object ProductDisconnected : BeaconState() + + /** + * Product does not support the beacon or the beacon is not connected + */ + object NotSupported : BeaconState() + + /** + * The beacon is connected to the product and is currently switched OFF + */ + object Inactive : BeaconState() + + /** + * The beacon is connected to the product and is currently switched ON + */ + object Active : BeaconState() + } + +} \ No newline at end of file diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/mfioconfig/MFIOConfigWidget.java b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/mfioconfig/MFIOConfigWidget.java new file mode 100644 index 00000000..6ccba975 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/mfioconfig/MFIOConfigWidget.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2018-2020 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.mfioconfig; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.Switch; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import dji.common.remotecontroller.ProfessionalRC; +import dji.log.DJILog; +import dji.ux.beta.accessory.R; +import dji.ux.beta.core.base.DJISDKModel; +import dji.ux.beta.core.base.SchedulerProvider; +import dji.ux.beta.core.base.widget.ConstraintLayoutWidget; +import dji.ux.beta.core.communication.ObservableInMemoryKeyedStore; + +public class MFIOConfigWidget extends ConstraintLayoutWidget + implements View.OnClickListener, Switch.OnCheckedChangeListener { + + //region Fields + private static final String TAG = "MFIOConfigWidget"; + private static final int PORT_1_OPEN_DUTY_RATIO = 60; + private static final int PORT_2_OPEN_DUTY_RATIO = 76; + private static final int PORT_3_OPEN_DUTY_RATIO = 70; + private static final int PORT_1_CLOSE_DUTY_RATIO = 40; + private static final int PORT_2_CLOSE_DUTY_RATIO = 58; + private static final int PORT_3_CLOSE_DUTY_RATIO = 54; + private MFIOConfigWidgetModel widgetModel; + private Switch powerSupplyEnabledSwitch; + private Button port1Button; + private Button port2Button; + private Button port3Button; + private boolean isPort1Open; + private boolean isPort2Open; + private boolean isPort3Open; + //endregion + + //region Constructor + public MFIOConfigWidget(@NonNull Context context) { + super(context); + } + + public MFIOConfigWidget(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public MFIOConfigWidget(@NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void initView(@NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr) { + inflate(context, R.layout.uxsdk_widget_mfio_config, this); + setBackgroundResource(R.drawable.uxsdk_background_black_rectangle); + + powerSupplyEnabledSwitch = findViewById(R.id.switch_enable_power_supply); + powerSupplyEnabledSwitch.setOnCheckedChangeListener(this); + port1Button = findViewById(R.id.button_port_one); + port1Button.setOnClickListener(this); + port2Button = findViewById(R.id.button_port_two); + port2Button.setOnClickListener(this); + port3Button = findViewById(R.id.button_port_three); + port3Button.setOnClickListener(this); + + if (!isInEditMode()) { + widgetModel = new MFIOConfigWidgetModel(DJISDKModel.getInstance(), + ObservableInMemoryKeyedStore.getInstance()); + initializePowerSupplyEnabled(); + initializeIOPortsToClosed(); + initializeCustomizableRCButtons(); + } + } + //endregion + + //region Lifecycle + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!isInEditMode()) { + widgetModel.setup(); + } + } + + @Override + protected void onDetachedFromWindow() { + if (!isInEditMode()) { + widgetModel.cleanup(); + } + super.onDetachedFromWindow(); + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.button_port_one) { + togglePort1State(); + } else if (v.getId() == R.id.button_port_two) { + togglePort2State(); + } else if (v.getId() == R.id.button_port_three) { + togglePort3State(); + } + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + addDisposable(widgetModel.setPowerSupplyEnabled(isChecked).subscribe(() -> { + }, throwable -> { + powerSupplyEnabledSwitch.setChecked(!isChecked); + DJILog.e(TAG, "setPowerSupplyEnabled: " + throwable.getLocalizedMessage()); + })); + } + + @Override + protected void reactToModelChanges() { + addReaction(widgetModel.getRCButtonEvents() + .observeOn(SchedulerProvider.ui()) + .subscribe(this::updatePortState)); + } + + @NonNull + @Override + public String getIdealDimensionRatioString() { + return getResources().getString(R.string.uxsdk_widget_mfio_config_ratio); + } + //endregion + + //region Helpers + private void initializePowerSupplyEnabled() { + addDisposable(widgetModel.getPowerSupplyEnabled() + .observeOn(SchedulerProvider.ui()) + .subscribe(enabled -> powerSupplyEnabledSwitch.setChecked((boolean) enabled), + logErrorConsumer("MFIOConfigWidget", "PowerSupplyEnabled:"))); + } + + private void initializeIOPortsToClosed() { + closePort(PORT_1_CLOSE_DUTY_RATIO, 0, port1Button); + isPort1Open = false; + closePort(PORT_2_CLOSE_DUTY_RATIO, 1, port2Button); + isPort2Open = false; + closePort(PORT_3_CLOSE_DUTY_RATIO, 2, port3Button); + isPort3Open = false; + } + + private void initializeCustomizableRCButtons() { + addDisposable(widgetModel.rcCustomizeBGButton().subscribe(() -> { + }, logErrorConsumer(TAG, "setBGCustomization: "))); + addDisposable(widgetModel.rcCustomizeC3Button().subscribe(() -> { + }, logErrorConsumer(TAG, "setC3Customization: "))); + addDisposable(widgetModel.rcCustomizeC4Button().subscribe(() -> { + }, logErrorConsumer(TAG, "setC4Customization: "))); + } + + private void updatePortState(@NonNull ProfessionalRC.Event event) { + switch (event.getFunctionID()) { + case CUSTOM150: + togglePort1State(); + break; + case CUSTOM151: + togglePort2State(); + break; + case CUSTOM152: + togglePort3State(); + break; + default: + // Do nothing + break; + } + } + + private void togglePort1State() { + if (isPort1Open) { + closePort(PORT_1_CLOSE_DUTY_RATIO, 0, port1Button); + isPort1Open = false; + } else { + openPort(PORT_1_OPEN_DUTY_RATIO, 0, port1Button); + isPort1Open = true; + } + } + + private void togglePort2State() { + if (isPort2Open) { + closePort(PORT_2_CLOSE_DUTY_RATIO, 1, port2Button); + isPort2Open = false; + } else { + openPort(PORT_2_OPEN_DUTY_RATIO, 1, port2Button); + isPort2Open = true; + } + } + + private void togglePort3State() { + if (isPort3Open) { + closePort(PORT_3_CLOSE_DUTY_RATIO, 2, port3Button); + isPort3Open = false; + } else { + openPort(PORT_3_OPEN_DUTY_RATIO, 2, port3Button); + isPort3Open = true; + } + } + + private void closePort(int dutyRatio, int index, Button port) { + addDisposable(widgetModel.initOnboardIO(dutyRatio, index).subscribe(() -> { + }, logErrorConsumer(TAG, "setPortClosed: " + index))); + + port.setText(R.string.uxsdk_port_open); + } + + private void openPort(int dutyRatio, int index, Button port) { + addDisposable(widgetModel.initOnboardIO(dutyRatio, index).subscribe(() -> { + }, logErrorConsumer(TAG, "setPortOpened: " + index))); + + port.setText(R.string.uxsdk_port_close); + } + //endregion +} diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/mfioconfig/MFIOConfigWidgetModel.java b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/mfioconfig/MFIOConfigWidgetModel.java new file mode 100644 index 00000000..bc2d095f --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/mfioconfig/MFIOConfigWidgetModel.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2018-2020 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.mfioconfig; + +import androidx.annotation.NonNull; + +import dji.common.flightcontroller.IOStateOnBoard; +import dji.common.remotecontroller.ProfessionalRC; +import dji.keysdk.DJIKey; +import dji.keysdk.FlightControllerKey; +import dji.keysdk.RemoteControllerKey; +import dji.thirdparty.io.reactivex.Completable; +import dji.thirdparty.io.reactivex.Flowable; +import dji.thirdparty.io.reactivex.Single; +import dji.ux.beta.core.base.DJISDKModel; +import dji.ux.beta.core.base.SchedulerProvider; +import dji.ux.beta.core.base.WidgetModel; +import dji.ux.beta.core.communication.ObservableInMemoryKeyedStore; +import dji.ux.beta.core.util.DataProcessor; + +public class MFIOConfigWidgetModel extends WidgetModel { + + //region Fields + private static final int FREQUENCY_50_HZ = 50; + private final DataProcessor proButtonProcessor; + //endregion + + //region Constructor + public MFIOConfigWidgetModel(@NonNull DJISDKModel djiSdkModel, + @NonNull ObservableInMemoryKeyedStore uxKeyManager) { + super(djiSdkModel, uxKeyManager); + proButtonProcessor = + DataProcessor.create(new ProfessionalRC.Event(ProfessionalRC.ButtonAction.OTHER)); + } + //endregion + + //region Lifecycle + @Override + protected void inSetup() { + DJIKey buttonEventKey = + RemoteControllerKey.create(RemoteControllerKey.BUTTON_EVENT_OF_PROFESSIONAL_RC); + bindDataProcessor(buttonEventKey, proButtonProcessor); + } + + @Override + protected void inCleanup() { + // Do nothing + } + + @Override + protected void updateStates() { + // Do nothing + } + //endregion + + //region Data + @NonNull + public Flowable getRCButtonEvents() { + return proButtonProcessor.toFlowable(); + } + + @NonNull + public Single getPowerSupplyEnabled() { + DJIKey powerSupplyEnabledKey = + FlightControllerKey.create(FlightControllerKey.POWER_SUPPLY_PORT_ENABLED); + return djiSdkModel.getValue(powerSupplyEnabledKey); + } + + @NonNull + public Completable setPowerSupplyEnabled(boolean enabled) { + DJIKey powerSupplyEnabledKey = + FlightControllerKey.create(FlightControllerKey.POWER_SUPPLY_PORT_ENABLED); + return djiSdkModel.setValue(powerSupplyEnabledKey, enabled) + .subscribeOn(SchedulerProvider.io()); + } + + @NonNull + public Completable initOnboardIO(int dutyRatio, int index) { + DJIKey initIO = FlightControllerKey.create(FlightControllerKey.INIT_IO); + IOStateOnBoard ioStateOnBoard = + IOStateOnBoard.Builder.createInitialParams(dutyRatio, FREQUENCY_50_HZ); + return djiSdkModel.performAction(initIO, index, ioStateOnBoard) + .subscribeOn(SchedulerProvider.io()); + } + + @NonNull + public Completable rcCustomizeBGButton() { + DJIKey setCustomizableButtons = + RemoteControllerKey.create(RemoteControllerKey.CUSTOMIZE_BUTTON); + return djiSdkModel.performAction(setCustomizableButtons, + ProfessionalRC.CustomizableButton.BG, + ProfessionalRC.ButtonAction.CUSTOM150) + .subscribeOn(SchedulerProvider.io()); + } + + @NonNull + public Completable rcCustomizeC3Button() { + DJIKey setCustomizableButtons = + RemoteControllerKey.create(RemoteControllerKey.CUSTOMIZE_BUTTON); + return djiSdkModel.performAction(setCustomizableButtons, + ProfessionalRC.CustomizableButton.C3, + ProfessionalRC.ButtonAction.CUSTOM151) + .subscribeOn(SchedulerProvider.io()); + } + + @NonNull + public Completable rcCustomizeC4Button() { + DJIKey setCustomizableButtons = + RemoteControllerKey.create(RemoteControllerKey.CUSTOMIZE_BUTTON); + return djiSdkModel.performAction(setCustomizableButtons, + ProfessionalRC.CustomizableButton.C4, + ProfessionalRC.ButtonAction.CUSTOM152) + .subscribeOn(SchedulerProvider.io()); + } + //endregion +} diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKEnabledWidget.kt b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKEnabledWidget.kt index 20dcf579..41c503a8 100644 --- a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKEnabledWidget.kt +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKEnabledWidget.kt @@ -18,7 +18,7 @@ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. - * + * */ package dji.ux.beta.accessory.widget.rtk diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKEnabledWidgetModel.kt b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKEnabledWidgetModel.kt index 3beff70d..fd1cc46b 100644 --- a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKEnabledWidgetModel.kt +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKEnabledWidgetModel.kt @@ -18,7 +18,7 @@ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. - * + * */ package dji.ux.beta.accessory.widget.rtk diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKSatelliteStatusWidget.kt b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKSatelliteStatusWidget.kt index b4ed79e3..f2e804ca 100644 --- a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKSatelliteStatusWidget.kt +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKSatelliteStatusWidget.kt @@ -18,7 +18,7 @@ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. - * + * */ package dji.ux.beta.accessory.widget.rtk diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKSatelliteStatusWidgetModel.kt b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKSatelliteStatusWidgetModel.kt index 7108c488..b0afa465 100644 --- a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKSatelliteStatusWidgetModel.kt +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKSatelliteStatusWidgetModel.kt @@ -18,7 +18,7 @@ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. - * + * */ package dji.ux.beta.accessory.widget.rtk diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKWidget.kt b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKWidget.kt index 38da7b70..5bc9686c 100644 --- a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKWidget.kt +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKWidget.kt @@ -18,7 +18,7 @@ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. - * + * */ package dji.ux.beta.accessory.widget.rtk @@ -185,7 +185,7 @@ open class RTKWidget @JvmOverloads constructor( //region Constructor override fun initView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { inflate(context, R.layout.uxsdk_widget_rtk, this) - setBackgroundResource(R.drawable.uxsdk_background_black_rectangle) + background = background ?: getDrawable(R.drawable.uxsdk_background_black_rectangle) } init { diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKWidgetModel.kt b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKWidgetModel.kt index 80a72ccc..1b9cd4f3 100644 --- a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKWidgetModel.kt +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/rtk/RTKWidgetModel.kt @@ -18,7 +18,7 @@ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. - * + * */ package dji.ux.beta.accessory.widget.rtk diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/speaker/SpeakerControlWidget.java b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/speaker/SpeakerControlWidget.java new file mode 100644 index 00000000..0e1240e3 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/speaker/SpeakerControlWidget.java @@ -0,0 +1,1361 @@ +/* + * Copyright (c) 2018-2020 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.speaker; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.ImageView; +import android.widget.SeekBar; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.ColorInt; +import androidx.annotation.Dimension; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.StyleRes; +import androidx.appcompat.widget.AppCompatSeekBar; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import dji.common.accessory.SettingsDefinitions; +import dji.common.accessory.SpeakerState; +import dji.sdk.media.AudioMediaFile; +import dji.ux.beta.core.base.SchedulerProvider; +import dji.ux.beta.accessory.R; +import dji.ux.beta.core.base.DJISDKModel; +import dji.ux.beta.core.base.widget.ConstraintLayoutWidget; +import dji.ux.beta.core.communication.ObservableInMemoryKeyedStore; +import dji.ux.beta.core.communication.OnStateChangeCallback; +import dji.ux.beta.core.util.AudioRecorderHandler; + +import static dji.ux.beta.core.extension.TypedArrayExtensions.INVALID_RESOURCE; + +/** + * Widget can be used to control of state of Speaker accessory. + * The widget provides a way to record audio file. + * It also displays a list of playable audio files to choose from. + */ +public class SpeakerControlWidget extends ConstraintLayoutWidget implements OnClickListener, OnCheckedChangeListener, AppCompatSeekBar.OnSeekBarChangeListener, OnStateChangeCallback { + + //region Fields + private static final String TAG = "SpeakerCtlWidget"; + private TextView titleTextView; + private TextView speakerVolumeLabel; + private TextView instantPlayLabel; + private TextView playInLoopLabel; + private TextView persistFileLabel; + private static final Format DATE_FORMAT = new SimpleDateFormat("MMM dd HH:mm:ss", Locale.US); + private TextView recordTabTextView; + private TextView fileListTabTextView; + private ConstraintLayout recordButtonContainer; + private ConstraintLayout mediaFileListContainer; + private ImageView recordImageView; + private TextView recordStatusTextView; + private Switch playInLoopSwitch; + private Switch persistFileSwitch; + private Switch instantPlayFileSwitch; + private AppCompatSeekBar volumeSeekbar; + private SpeakerControlWidgetModel widgetModel; + private AudioFileListAdapter audioFileListAdapter; + private int currentPlayingFileIndex; + private SpeakerWidgetState speakerWidgetState = SpeakerWidgetState.BROADCAST; + private TimerRunnable recordingTimerTracker; + private Animation animation; + private float listTextSize; + @ColorInt + private int listTextColor; + private Drawable listTextBackground; + private Drawable listImageBackground; + private Drawable listDeleteIcon; + private Drawable listStartPlayIcon; + private Drawable listStopPlayIcon; + + + //endregion + + //region Lifecycle + public SpeakerControlWidget(Context context) { + super(context); + } + + public SpeakerControlWidget(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SpeakerControlWidget(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + inflate(context, R.layout.uxsdk_widget_speaker_control, this); + setBackgroundResource(R.drawable.uxsdk_background_black_rectangle); + titleTextView = findViewById(R.id.text_view_speaker_title); + speakerVolumeLabel = findViewById(R.id.text_view_speaker_volume); + instantPlayLabel = findViewById(R.id.text_view_instant_play); + playInLoopLabel = findViewById(R.id.text_view_loop_mode); + persistFileLabel = findViewById(R.id.text_view_persist_file); + recordTabTextView = findViewById(R.id.tab_instant_broadcast); + fileListTabTextView = findViewById(R.id.tab_local_file); + recordButtonContainer = findViewById(R.id.record_button_container); + mediaFileListContainer = findViewById(R.id.file_list_container); + recordImageView = findViewById(R.id.start_broadcast_button); + recordStatusTextView = findViewById(R.id.audio_record_status_text_view); + playInLoopSwitch = findViewById(R.id.loop_play_switch); + persistFileSwitch = findViewById(R.id.audio_temporary_switch); + instantPlayFileSwitch = findViewById(R.id.instant_play_switch); + volumeSeekbar = findViewById(R.id.volume_seek_bar); + RecyclerView recyclerView = findViewById(R.id.audio_file_list_view); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + audioFileListAdapter = new AudioFileListAdapter(); + recyclerView.setAdapter(audioFileListAdapter); + recordTabTextView.setOnClickListener(this); + fileListTabTextView.setOnClickListener(this); + recordImageView.setOnClickListener(this); + playInLoopSwitch.setOnCheckedChangeListener(this); + persistFileSwitch.setOnCheckedChangeListener(this); + instantPlayFileSwitch.setOnCheckedChangeListener(this); + volumeSeekbar.setOnSeekBarChangeListener(this); + + AudioRecorderHandler audioRecorderHandler = new AudioRecorderHandler(context); + if (!isInEditMode()) { + widgetModel = new SpeakerControlWidgetModel(DJISDKModel.getInstance(), + ObservableInMemoryKeyedStore.getInstance(), + audioRecorderHandler); + } + speakerWidgetState = SpeakerWidgetState.BROADCAST; + setPanelState(); + + if (attrs != null) { + initAttributes(context, attrs); + } + + } + + + @Override + protected void reactToModelChanges() { + addReaction(widgetModel.getSpeakerVolume() + .observeOn(SchedulerProvider.ui()) + .subscribe(this::onSpeakerVolumeChanged)); + + addReaction(widgetModel.getMediaFileList() + .observeOn(SchedulerProvider.ui()) + .subscribe(this::onMediaListChanged)); + + addReaction(widgetModel.getSpeakerState() + .observeOn(SchedulerProvider.ui()) + .subscribe(this::onSpeakerStateChanged)); + + addReaction(widgetModel.isRecording() + .observeOn(SchedulerProvider.ui()) + .subscribe(this::onRecording)); + } + + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!isInEditMode()) { + widgetModel.setup(); + } + } + + @Override + protected void onDetachedFromWindow() { + if (!isInEditMode()) { + widgetModel.cleanup(); + } + super.onDetachedFromWindow(); + } + + + @NonNull + @Override + public String getIdealDimensionRatioString() { + return getResources().getString(R.string.uxsdk_widget_speaker_control_ratio); + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.start_broadcast_button) { + checkAndStartRecording(); + } else if (id == R.id.audio_file_play_image_view) { + playAudioInList((int) v.getTag()); + } else if (id == R.id.audio_file_delete_image_view) { + deleteOneFileByIndex((int) v.getTag()); + } else if (id == R.id.tab_instant_broadcast) { + speakerWidgetState = SpeakerWidgetState.BROADCAST; + setPanelState(); + } else if (id == R.id.tab_local_file) { + speakerWidgetState = SpeakerWidgetState.LOCAL_FILE_LIST; + setPanelState(); + } + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (buttonView.equals(playInLoopSwitch)) { + addDisposable(widgetModel.setLoopModeEnabled(isChecked).subscribe(() -> { + }, logErrorConsumer(TAG, "set loop enabled "))); + } else if (buttonView.equals(instantPlayFileSwitch)) { + widgetModel.setInstantPlayEnabled(isChecked); + } else if (buttonView.equals(persistFileSwitch)) { + widgetModel.setPersistFileEnabled(isChecked); + } + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (seekBar.equals(volumeSeekbar) && fromUser) { + handleSeekbarChanged(progress); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + //empty method + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + //empty method + } + + + @Override + public void onStateChange(@Nullable Object state) { + toggleVisibility(); + } + //endregion + + //region private helpers + + private void toggleVisibility() { + if (getVisibility() == VISIBLE) { + setVisibility(GONE); + } else { + setVisibility(VISIBLE); + } + } + + private void onSpeakerVolumeChanged(int integer) { + volumeSeekbar.setProgress(integer); + } + + + private void onRecording(Boolean isRecording) { + if (isRecording) { + + if (animation != null) { + recordImageView.startAnimation(animation); + } + + if (recordingTimerTracker != null) { + recordingTimerTracker = null; + } + recordingTimerTracker = new TimerRunnable(SystemClock.uptimeMillis(), true); + getHandler().post(recordingTimerTracker); + + } else { + if (recordingTimerTracker != null) { + recordingTimerTracker.setRecording(false); + recordImageView.clearAnimation(); + } + + } + + } + + private void initAttributes(Context context, AttributeSet attrs) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SpeakerControlWidget); + + int textAppearance = typedArray.getResourceId(R.styleable.SpeakerControlWidget_uxsdk_widgetTitleTextAppearance, INVALID_RESOURCE); + if (textAppearance != INVALID_RESOURCE) { + setTitleTextAppearance(textAppearance); + } + setTitleTextColor(typedArray.getColor(R.styleable.SpeakerControlWidget_uxsdk_widgetTitleTextColor, Color.WHITE)); + setTitleBackground(typedArray.getDrawable(R.styleable.SpeakerControlWidget_uxsdk_widgetTitleBackground)); + setTitleTextSize(typedArray.getDimension(R.styleable.SpeakerControlWidget_uxsdk_widgetTitleTextSize, 14)); + textAppearance = typedArray.getResourceId(R.styleable.SpeakerControlWidget_uxsdk_labelsTextAppearance, INVALID_RESOURCE); + if (textAppearance != INVALID_RESOURCE) { + setLabelsTextAppearance(textAppearance); + } + setLabelsTextColor(typedArray.getColor(R.styleable.SpeakerControlWidget_uxsdk_labelsTextColor, Color.WHITE)); + setLabelsBackground(typedArray.getDrawable(R.styleable.SpeakerControlWidget_uxsdk_labelsBackground)); + setLabelsTextSize(typedArray.getDimension(R.styleable.SpeakerControlWidget_uxsdk_labelsTextSize, 12)); + textAppearance = typedArray.getResourceId(R.styleable.SpeakerControlWidget_uxsdk_recordTimerTextAppearance, INVALID_RESOURCE); + if (textAppearance != INVALID_RESOURCE) { + setRecordTimerTextAppearance(textAppearance); + } + setRecordTimerTextColor(typedArray.getColor(R.styleable.SpeakerControlWidget_uxsdk_recordTimerTextColor, Color.WHITE)); + setRecordTimerTextBackground(typedArray.getDrawable(R.styleable.SpeakerControlWidget_uxsdk_recordTimerTextBackground)); + setRecordTimerTextSize(typedArray.getDimension(R.styleable.SpeakerControlWidget_uxsdk_recordTimerTextSize, 12)); + textAppearance = typedArray.getResourceId(R.styleable.SpeakerControlWidget_uxsdk_tabTextAppearance, INVALID_RESOURCE); + if (textAppearance != INVALID_RESOURCE) { + setTabTextAppearance(textAppearance); + } + ColorStateList colorStateList = typedArray.getColorStateList(R.styleable.SpeakerControlWidget_uxsdk_tabTextColor); + if (colorStateList != null) { + setTabTextColors(colorStateList); + } + setBroadcastTabBackgroundSelector((typedArray.getResourceId(R.styleable.SpeakerControlWidget_uxsdk_tabBroadcastBackgroundSelector, R.drawable.uxsdk_selector_speaker_widget_broadcast_tab))); + setFileListTabBackgroundSelector((typedArray.getResourceId(R.styleable.SpeakerControlWidget_uxsdk_tabFileListBackgroundSelector, R.drawable.uxsdk_selector_speaker_widget_local_file_tab))); + setTabTextSize(typedArray.getDimension(R.styleable.SpeakerControlWidget_uxsdk_tabTextSize, 12)); + + setFileListTextColor(typedArray.getColor(R.styleable.SpeakerControlWidget_uxsdk_fileListTextColor, Color.WHITE)); + setFileListTextBackground(typedArray.getDrawable(R.styleable.SpeakerControlWidget_uxsdk_fileListTextBackground)); + setFileListTextSize(typedArray.getDimension(R.styleable.SpeakerControlWidget_uxsdk_fileListTextSize, 8)); + if (typedArray.getDrawable(R.styleable.SpeakerControlWidget_uxsdk_fileListDeleteIcon) != null) { + setFileListDeleteIcon(typedArray.getDrawable(R.styleable.SpeakerControlWidget_uxsdk_fileListDeleteIcon)); + } + if (typedArray.getDrawable(R.styleable.SpeakerControlWidget_uxsdk_fileListPlayIcon) != null) { + setFileListPlayIcon(typedArray.getDrawable(R.styleable.SpeakerControlWidget_uxsdk_fileListPlayIcon)); + } + if (typedArray.getDrawable(R.styleable.SpeakerControlWidget_uxsdk_fileListStopIcon) != null) { + setFileListStopPlayIcon(typedArray.getDrawable(R.styleable.SpeakerControlWidget_uxsdk_fileListStopIcon)); + } + + animation = AnimationUtils.loadAnimation(context, R.anim.uxsdk_anim_blink); + typedArray.recycle(); + } + + private void setPanelState() { + if (speakerWidgetState == SpeakerWidgetState.BROADCAST) { + recordTabTextView.setSelected(true); + + fileListTabTextView.setSelected(false); + + mediaFileListContainer.setVisibility(View.GONE); + recordButtonContainer.setVisibility(VISIBLE); + + } else if (speakerWidgetState == SpeakerWidgetState.LOCAL_FILE_LIST) { + fileListTabTextView.setSelected(true); + + recordTabTextView.setSelected(false); + mediaFileListContainer.setVisibility(View.VISIBLE); + recordButtonContainer.setVisibility(GONE); + } + } + + + private void onSpeakerStateChanged(SpeakerState speakerState) { + currentPlayingFileIndex = speakerState.getPlayingIndex(); + if (speakerState.getPlayingMode() == SettingsDefinitions.PlayMode.SINGLE_ONCE) { + playInLoopSwitch.setChecked(false); + } else if (speakerState.getPlayingMode() == SettingsDefinitions.PlayMode.REPEAT_SINGLE) { + playInLoopSwitch.setChecked(true); + } + if (audioFileListAdapter != null) { + audioFileListAdapter.notifyDataSetChanged(); + } + } + + + private void onMediaListChanged(List audioMediaFiles) { + audioFileListAdapter.setMediaFileList(audioMediaFiles); + audioFileListAdapter.notifyDataSetChanged(); + + } + + private void playAudioInList(int index) { + addDisposable(widgetModel.playFile(index).subscribe(() -> { + }, logErrorConsumer(TAG, "Play file "))); + + } + + private void deleteOneFileByIndex(int index) { + addDisposable(widgetModel.deleteOneFileByIndex(index).subscribe(() -> { + }, logErrorConsumer(TAG, "Delete file "))); + } + + + private void handleSeekbarChanged(int progress) { + addDisposable(widgetModel.setSpeakerVolume(progress).subscribe(() -> { + }, logErrorConsumer(TAG, "Set Volume "))); + } + + private void checkAndStartRecording() { + if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(getContext(), + getResources().getString(R.string.uxsdk_speaker_record_permission_required), + Toast.LENGTH_LONG).show(); + return; + } + + addDisposable(widgetModel.isRecording().firstOrError() + .observeOn(SchedulerProvider.ui()) + .subscribe( + isRecording -> { + if (isRecording) { + stopRecordAction(); + } else { + startRecordAction(); + } + + })); + + } + + private void startRecordAction() { + addDisposable(widgetModel.startRecording().subscribe(() -> { + }, logErrorConsumer(TAG, "Start record "))); + } + + private void stopRecordAction() { + addDisposable(widgetModel.stopRecording().subscribe(() -> { + }, logErrorConsumer(TAG, "Stop record "))); + } + + + private static class ItemHolder extends RecyclerView.ViewHolder { + private ImageView playFileImageView; + private ImageView deleteFileImageView; + private TextView fileNameTextView; + + public ItemHolder(View convertView) { + super(convertView); + this.playFileImageView = convertView.findViewById(R.id.audio_file_play_image_view); + this.deleteFileImageView = convertView.findViewById(R.id.audio_file_delete_image_view); + this.fileNameTextView = convertView.findViewById(R.id.audio_file_name_text_view); + + } + } + + + private class AudioFileListAdapter extends RecyclerView.Adapter { + @Nullable + private List mediaFileList; + + public void setMediaFileList(@Nullable List mediaFileList) { + this.mediaFileList = mediaFileList; + } + + @Override + @NonNull + public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.uxsdk_item_audio_file, parent, false); + ItemHolder itemHolder = new ItemHolder(view); + listTextSize = itemHolder.fileNameTextView.getTextSize(); + listTextBackground = itemHolder.fileNameTextView.getBackground(); + listTextColor = itemHolder.fileNameTextView.getCurrentTextColor(); + listImageBackground = itemHolder.deleteFileImageView.getBackground(); + listStartPlayIcon = getResources().getDrawable(R.drawable.uxsdk_selector_speaker_start_play); + listStopPlayIcon = getResources().getDrawable(R.drawable.uxsdk_selector_speaker_stop_play); + listDeleteIcon = getResources().getDrawable(R.drawable.uxsdk_selector_icon_delete); + return new ItemHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ItemHolder holder, int position) { + if (mediaFileList != null) { + final AudioMediaFile mediaFile = mediaFileList.get(position); + if (mediaFile != null) { + holder.fileNameTextView.setTextSize(listTextSize); + holder.fileNameTextView.setBackground(listTextBackground); + holder.fileNameTextView.setTextColor(listTextColor); + holder.deleteFileImageView.setBackground(listImageBackground); + holder.playFileImageView.setBackground(listImageBackground); + + if (mediaFile.getFileName() == null) { + Date date = new Date(mediaFile.getTimeCreated()); + holder.fileNameTextView.setText(getResources().getString(R.string.uxsdk_speaker_panel_list_prefix, DATE_FORMAT.format(date))); + } else { + holder.fileNameTextView.setText(mediaFile.getFileName()); + } + + holder.playFileImageView.setOnClickListener(SpeakerControlWidget.this); + holder.deleteFileImageView.setOnClickListener(SpeakerControlWidget.this); + if (currentPlayingFileIndex == mediaFile.getIndex()) { + holder.playFileImageView.setImageDrawable(listStopPlayIcon); + } else { + holder.playFileImageView.setImageDrawable(listStartPlayIcon); + } + holder.deleteFileImageView.setImageDrawable(listDeleteIcon); + holder.playFileImageView.setTag(position); + holder.deleteFileImageView.setTag(position); + + } + } + } + + + @Override + public int getItemCount() { + if (mediaFileList != null) { + return mediaFileList.size(); + } + return 0; + } + } + + private class TimerRunnable implements Runnable { + private boolean isRecording; + private long startTime; + + public TimerRunnable(long startTime, boolean isRecording) { + this.startTime = startTime; + this.isRecording = isRecording; + } + + public void setRecording(boolean recording) { + isRecording = recording; + } + + @Override + public void run() { + long millisecondTime = SystemClock.uptimeMillis() - startTime; + int seconds = (int) (millisecondTime / 1000); + int minutes = seconds / 60; + recordStatusTextView.setText(String.format(Locale.US, "%d:%02d", minutes, seconds)); + if (isRecording) { + getHandler().postDelayed(this, 0); + } else { + recordStatusTextView.setText(getResources().getString(R.string.uxsdk_speaker_panel_tap_to_record)); + } + } + } + + private enum SpeakerWidgetState { + BROADCAST(0), + LOCAL_FILE_LIST(1), + UNKNOWN(10); + + private final int mValue; + + SpeakerWidgetState(int value) { + mValue = value; + } + + private static SpeakerWidgetState[] values; + + public static SpeakerWidgetState[] getValues() { + if (values == null) { + values = values(); + } + return values; + } + + public static SpeakerWidgetState find(int b) { + SpeakerWidgetState result = UNKNOWN; + for (int i = 0; i < getValues().length; i++) { + if (getValues()[i]._equals(b)) { + result = getValues()[i]; + break; + } + } + return result; + } + + public int value() { + return mValue; + } + + private boolean _equals(int b) { + return mValue == b; + } + } + + //endregion + + //region title customizations + + /** + * Set the text color state list of the title + * + * @param colorStateList to be used for title + */ + public void setTitleTextColor(@Nullable ColorStateList colorStateList) { + titleTextView.setTextColor(colorStateList); + } + + /** + * Set the text color of title + * + * @param color integer value + */ + public void setTitleTextColor(@ColorInt int color) { + titleTextView.setTextColor(color); + } + + /** + * Get the text color state list of the titles + * + * @return ColorStateList + */ + @Nullable + public ColorStateList getTitleTextColors() { + return titleTextView.getTextColors(); + } + + /** + * Get the text color of title + * + * @return integer value representing color + */ + @ColorInt + public int getTitleTextColor() { + return titleTextView.getCurrentTextColor(); + } + + /** + * Set the text appearance of title + * + * @param textAppearance to be used + */ + public void setTitleTextAppearance(@StyleRes int textAppearance) { + titleTextView.setTextAppearance(getContext(), textAppearance); + } + + /** + * Set the background of the title + * + * @param resourceId to be used + */ + public void setTitleBackground(@DrawableRes int resourceId) { + setTitleBackground(getResources().getDrawable(resourceId)); + } + + /** + * Set the background of the title + * + * @param drawable to be used + */ + public void setTitleBackground(@Nullable Drawable drawable) { + titleTextView.setBackground(drawable); + } + + /** + * Get the background of the title + * + * @return Drawable + */ + @Nullable + public Drawable getTitleBackground() { + return titleTextView.getBackground(); + } + + /** + * Set the size of the title text + * + * @param textSize float value + */ + public void setTitleTextSize(@Dimension float textSize) { + titleTextView.setTextSize(textSize); + } + + /** + * Get the size of title text + * + * @return float value representing text size + */ + @Dimension + public float getTitleTextSize() { + return titleTextView.getTextSize(); + } + + //endregion + + //region label customizations + + /** + * Set the color state list for labels + * + * @param colorStateList to be used + */ + public void setLabelsTextColor(@Nullable ColorStateList colorStateList) { + speakerVolumeLabel.setTextColor(colorStateList); + playInLoopLabel.setTextColor(colorStateList); + persistFileLabel.setTextColor(colorStateList); + instantPlayLabel.setTextColor(colorStateList); + } + + /** + * Set the text color to be used for labels + * + * @param color integer value representing color + */ + public void setLabelsTextColor(@ColorInt int color) { + speakerVolumeLabel.setTextColor(color); + playInLoopLabel.setTextColor(color); + persistFileLabel.setTextColor(color); + instantPlayLabel.setTextColor(color); + } + + /** + * Get the text colors for labels + * + * @return the color state list being used as labels + */ + @Nullable + public ColorStateList getLabelsTextColors() { + return speakerVolumeLabel.getTextColors(); + } + + /** + * Get the text color of labels + * + * @return integer value representing color + */ + @ColorInt + public int getLabelsTextColor() { + return speakerVolumeLabel.getCurrentTextColor(); + } + + /** + * Set the text appearance of labels + * + * @param textAppearance to be used + */ + public void setLabelsTextAppearance(@StyleRes int textAppearance) { + speakerVolumeLabel.setTextAppearance(getContext(), textAppearance); + playInLoopLabel.setTextAppearance(getContext(), textAppearance); + persistFileLabel.setTextAppearance(getContext(), textAppearance); + instantPlayLabel.setTextAppearance(getContext(), textAppearance); + } + + /** + * Set the background of labels + * + * @param resourceId to be used + */ + public void setLabelsBackground(@DrawableRes int resourceId) { + setLabelsBackground(getResources().getDrawable(resourceId)); + } + + /** + * Set the background of labels + * + * @param drawable to be used + */ + public void setLabelsBackground(@Nullable Drawable drawable) { + speakerVolumeLabel.setBackground(drawable); + playInLoopLabel.setBackground(drawable); + persistFileLabel.setBackground(drawable); + instantPlayLabel.setBackground(drawable); + } + + /** + * Get the background of labels + * + * @return Drawable + */ + @Nullable + public Drawable getLabelsBackground() { + return speakerVolumeLabel.getBackground(); + } + + /** + * Set the text size of labels + * + * @param textSize float value + */ + public void setLabelsTextSize(@Dimension float textSize) { + speakerVolumeLabel.setTextSize(textSize); + playInLoopLabel.setTextSize(textSize); + persistFileLabel.setTextSize(textSize); + instantPlayLabel.setTextSize(textSize); + } + + /** + * Get the text size of labels + * + * @return float value representing text size + */ + @Dimension + public float getLabelsTextSize() { + return speakerVolumeLabel.getTextSize(); + } + + //endregion + + //region tab customizations + + /** + * Set the broadcast tab background selector + *

+ * android:state_selected="true" for selected state + * android:state_selected="false" for not selected state + * + * @param resourceId to be used + */ + public void setBroadcastTabBackgroundSelector(@DrawableRes int resourceId) { + recordTabTextView.setBackgroundResource(resourceId); + } + + /** + * Set the file list tab background selector + *

+ * android:state_selected="true" for selected state + * android:state_selected="false" for not selected state + * + * @param resourceId to be used + */ + public void setFileListTabBackgroundSelector(@DrawableRes int resourceId) { + fileListTabTextView.setBackgroundResource(resourceId); + } + + /** + * Get the background of the broadcast tab + * + * @return Drawable + */ + @Nullable + public Drawable getBroadcastTabBackground() { + return recordTabTextView.getBackground(); + } + + /** + * Get the background of the file list tab + * + * @return Drawable + */ + @Nullable + public Drawable getFileListTabBackground() { + return fileListTabTextView.getBackground(); + } + + /** + * Set the text color state list for tab text + * + * @param colorStateList to be used + */ + public void setTabTextColors(@NonNull ColorStateList colorStateList) { + recordTabTextView.setTextColor(colorStateList); + fileListTabTextView.setTextColor(colorStateList); + } + + /** + * Get the text color state list for tab text + * + * @return ColorStateList + */ + @Nullable + public ColorStateList getTabTextColors() { + return recordTabTextView.getTextColors(); + } + + /** + * Set the text appearance of tab text + * + * @param textAppearance to be used + */ + public void setTabTextAppearance(@StyleRes int textAppearance) { + recordTabTextView.setTextAppearance(getContext(), textAppearance); + fileListTabTextView.setTextAppearance(getContext(), textAppearance); + } + + /** + * Set the text size of tab text + * + * @param textSize float value + */ + public void setTabTextSize(@Dimension float textSize) { + recordTabTextView.setTextSize(textSize); + fileListTabTextView.setTextSize(textSize); + } + + /** + * Get the text size of tab text + * + * @return float value representing text size + */ + @Dimension + public float getTabTextSize() { + return recordTabTextView.getTextSize(); + } + + //endregion + + //region record button customizations + + /** + * Set the icon for record audio button + * + * @param resourceId to be used + */ + public void setRecordButtonIcon(@DrawableRes int resourceId) { + recordImageView.setImageResource(resourceId); + } + + /** + * Set the icon for record audio button + * + * @param drawable to be used + */ + public void setRecordButtonIcon(@Nullable Drawable drawable) { + recordImageView.setImageDrawable(drawable); + } + + /** + * Get the icon of record audio button + * + * @return Drawable + */ + @Nullable + public Drawable getRecordButtonIcon() { + return recordImageView.getDrawable(); + } + + /** + * Set the background of the record audio button + * + * @param resourceId to be used + */ + public void setRecordButtonBackground(@DrawableRes int resourceId) { + recordImageView.setBackgroundResource(resourceId); + } + + /** + * Set the background of the record audio button + * + * @param drawable to be used + */ + public void setRecordButtonBackground(@Nullable Drawable drawable) { + recordImageView.setBackground(drawable); + } + + /** + * Get the background of the record audio button + * + * @return Drawable + */ + @Nullable + public Drawable getRecordButtonBackground() { + return recordImageView.getBackground(); + } + + /** + * Set the animation for audio recording + * + * @param animation to be used + */ + public void setRecordingAnimation(@Nullable Animation animation) { + this.animation = animation; + } + + /** + * Set the background of record duration timer text + * + * @param resourceId to be used + */ + public void setRecordTimerTextBackground(@DrawableRes int resourceId) { + recordStatusTextView.setBackgroundResource(resourceId); + } + + /** + * Set the background of record duration timer text + * + * @param drawable to be used + */ + public void setRecordTimerTextBackground(@Nullable Drawable drawable) { + recordStatusTextView.setBackground(drawable); + } + + /** + * Set the color state list of the record duration timer text + * + * @param colorStateList to be used + */ + public void setRecordTimerTextColors(@Nullable ColorStateList colorStateList) { + recordStatusTextView.setTextColor(colorStateList); + } + + /** + * Set the color of the record duration text + * + * @param color integer value + */ + public void setRecordTimerTextColor(@ColorInt int color) { + recordStatusTextView.setTextColor(color); + } + + /** + * Get the color state list of the record duration text + * + * @return ColorStateList + */ + @Nullable + public ColorStateList getRecordTimerTextColors() { + return recordStatusTextView.getTextColors(); + } + + /** + * Get the color of the record duration text + * + * @return integer value representing color + */ + @ColorInt + public int getRecordTimerTextColor() { + return recordStatusTextView.getCurrentTextColor(); + } + + /** + * Set the size of the record duration text + * + * @param textSize float value + */ + public void setRecordTimerTextSize(@Dimension float textSize) { + recordStatusTextView.setTextSize(textSize); + } + + + /** + * Get the size of the record duration text + * + * @return float value representing text size + */ + @Dimension + public float getRecordTimerTextSize() { + return recordStatusTextView.getTextSize(); + } + + /** + * Set the appearance of the record duration text + * + * @param textAppearance to be used + */ + public void setRecordTimerTextAppearance(@StyleRes int textAppearance) { + recordStatusTextView.setTextAppearance(getContext(), textAppearance); + } + + //endregion + + //region Seekbar customizations + + /** + * Set the volume seekbar thumb icon + * + * @param resourceId to be used + */ + public void setVolumeSeekbarThumbDrawable(@DrawableRes int resourceId) { + setVolumeSeekbarThumbDrawable(getResources().getDrawable(resourceId)); + } + + /** + * Set the volume seekbar thumb icon + * + * @param drawable to be used + */ + public void setVolumeSeekbarThumbDrawable(@NonNull Drawable drawable) { + volumeSeekbar.setThumb(drawable); + } + + /** + * Get the drawable used as the volume seekbar thumb icon + * + * @return Drawable + */ + @Nullable + public Drawable getVolumeSeekbarThumbDrawable() { + return volumeSeekbar.getThumb(); + } + + /** + * Get the tint list for volume seek bar background + * + * @return ColorStateList + */ + @Nullable + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public ColorStateList getVolumeSeekbarBackgroundTintList() { + return volumeSeekbar.getBackgroundTintList(); + } + + /** + * Set the tint list for the volume seek bar background + * + * @param colorStateList to be used + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public void setVolumeSeekbarBackgroundTintList(@Nullable ColorStateList colorStateList) { + volumeSeekbar.setBackgroundTintList(colorStateList); + } + + /** + * Set the tint list for the volume seek bar progress + * + * @param colorStateList to be used + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public void setVolumeSeekbarProgressTintList(@Nullable ColorStateList colorStateList) { + volumeSeekbar.setProgressTintList(colorStateList); + } + + /** + * Get the tint list for the volume seek bar progress + * + * @return ColorStateList + */ + @Nullable + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public ColorStateList getVolumeSeekbarProgressTintList() { + return volumeSeekbar.getProgressTintList(); + } + //endregion + + //region Switch customizations + + /** + * Set the thumb icon used for the switches + * + * @param resourceId to be used + */ + public void setSwitchThumb(@DrawableRes int resourceId) { + playInLoopSwitch.setThumbResource(resourceId); + persistFileSwitch.setThumbResource(resourceId); + instantPlayFileSwitch.setThumbResource(resourceId); + } + + /** + * Set the thumb icon used for the switches + * + * @param drawable to be used + */ + public void setSwitchThumb(@Nullable Drawable drawable) { + playInLoopSwitch.setThumbDrawable(drawable); + persistFileSwitch.setThumbDrawable(drawable); + instantPlayFileSwitch.setThumbDrawable(drawable); + } + + /** + * Get the thumb icon used by the switches + * + * @return Drawable + */ + @Nullable + public Drawable getSwitchThumb() { + return playInLoopSwitch.getThumbDrawable(); + } + + /** + * Set the switch thumb tint list + * + * @param colorStateList to be used + */ + @RequiresApi(Build.VERSION_CODES.M) + public void setSwitchThumbTintList(@Nullable ColorStateList colorStateList) { + playInLoopSwitch.setThumbTintList(colorStateList); + persistFileSwitch.setThumbTintList(colorStateList); + instantPlayFileSwitch.setThumbTintList(colorStateList); + } + + /** + * Get the switch thumb tint list + * + * @return ColorStateList + */ + @Nullable + @RequiresApi(Build.VERSION_CODES.M) + public ColorStateList getSwitchThumbTintList() { + return playInLoopSwitch.getThumbTintList(); + } + + //endregion + + //region file list customizations + + /** + * Set the color of the file name text in the audio file list + * + * @param color integer value representing color + */ + public void setFileListTextColor(@ColorInt int color) { + listTextColor = color; + } + + /** + * Get the color of the file name text in the audio file list + * + * @return integer value representing color + */ + @ColorInt + public int getFileListTextColor() { + return listTextColor; + } + + /** + * Set the background of file name text in the audio file list + * + * @param resourceId to be used + */ + public void setFileListTextBackground(@DrawableRes int resourceId) { + setFileListTextBackground(getResources().getDrawable(resourceId)); + } + + /** + * Set the background of file name text in the audio file list + * + * @param drawable to be used + */ + public void setFileListTextBackground(@Nullable Drawable drawable) { + listTextBackground = drawable; + } + + + /** + * Get the file name text background in the audio file list + * + * @return Drawable + */ + @Nullable + public Drawable getFileListTextBackground() { + return listTextBackground; + } + + /** + * Set the size of the file name text in the audio file list + * + * @param textSize float value + */ + public void setFileListTextSize(@Dimension float textSize) { + listTextSize = textSize; + } + + /** + * Get the size of file name text in the audio file list + * + * @return float representing text size + */ + @Dimension + public float getFileListTextSize() { + return listTextSize; + } + + /** + * Set the icon for the delete button in the audio file list + * + * @param resourceId to be used + */ + public void setFileListDeleteIcon(@DrawableRes int resourceId) { + setFileListDeleteIcon(getResources().getDrawable(resourceId)); + } + + /** + * Set the icon for the delete button in the audio file list + * + * @param drawable to be used + */ + public void setFileListDeleteIcon(@Nullable Drawable drawable) { + listDeleteIcon = drawable; + } + + /** + * Get the icon for the delete button displayed in the audio file list + * + * @return Drawable + */ + @Nullable + public Drawable getFileListDeleteIcon() { + return listDeleteIcon; + } + + /** + * Set the icon for the start play button in the audio file list + * + * @param resourceId to be used + */ + public void setFileListPlayIcon(@DrawableRes int resourceId) { + setFileListPlayIcon(getResources().getDrawable(resourceId)); + } + + /** + * Set the icon for the start play button in the audio file list + * + * @param drawable to be used + */ + public void setFileListPlayIcon(@Nullable Drawable drawable) { + listStartPlayIcon = drawable; + } + + /** + * Get the icon for the start play button displayed in the audio file list + * + * @return Drawable + */ + @Nullable + public Drawable getFileListPlayIcon() { + return listStartPlayIcon; + } + + /** + * Set the icon for the stop play button in the audio file list + * + * @param resourceId to be used + */ + public void setFileListStopPlayIcon(@DrawableRes int resourceId) { + setFileListStopPlayIcon(getResources().getDrawable(resourceId)); + } + + /** + * Set the icon for the stop play button in the audio file list + * + * @param drawable to be used + */ + public void setFileListStopPlayIcon(@Nullable Drawable drawable) { + listStopPlayIcon = drawable; + } + + /** + * Get the stop play icon displayed in the audio file list + * + * @return Drawable + */ + @Nullable + public Drawable getFileListStopPlayIcon() { + return listStopPlayIcon; + } + + //endregion + + +} diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/speaker/SpeakerControlWidgetModel.java b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/speaker/SpeakerControlWidgetModel.java new file mode 100644 index 00000000..2e4b09de --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/speaker/SpeakerControlWidgetModel.java @@ -0,0 +1,442 @@ +/* + * Copyright (c) 2018-2020 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.speaker; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Locale; + +import dji.common.accessory.SettingsDefinitions; +import dji.common.accessory.SpeakerState; +import dji.common.error.DJIError; +import dji.common.error.DJISDKError; +import dji.common.util.CommonCallbacks; +import dji.keysdk.AccessoryAggregationKey; +import dji.keysdk.DJIKey; +import dji.log.DJILog; +import dji.sdk.accessory.speaker.AudioFileInfo; +import dji.sdk.accessory.speaker.Speaker; +import dji.sdk.accessory.speaker.TransmissionListener; +import dji.sdk.media.AudioMediaFile; +import dji.sdk.media.MediaManager; +import dji.thirdparty.io.reactivex.Completable; +import dji.thirdparty.io.reactivex.Flowable; +import dji.ux.beta.core.base.DJISDKModel; +import dji.ux.beta.core.base.SchedulerProvider; +import dji.ux.beta.core.base.UXSDKError; +import dji.ux.beta.core.base.WidgetModel; +import dji.ux.beta.core.communication.ObservableInMemoryKeyedStore; +import dji.ux.beta.core.util.AudioRecorderHandler; +import dji.ux.beta.core.util.DataProcessor; +import dji.ux.beta.core.util.ProductUtil; + +/** + * Speaker Control Widget Model + *

+ * Widget Model for the {@link SpeakerControlWidget} used to define the + * underlying logic and communication + */ +public class SpeakerControlWidgetModel extends WidgetModel implements SpeakerState.Callback, + MediaManager.FileListStateListener, AudioRecorderHandler.AudioRecordingCallback, TransmissionListener { + //region Fields + private static final String TAG = "SpeakerCtlWidgetModel"; + private static final int DEFAULT_VOLUME = 0; + @Nullable + private Speaker speaker; + @Nullable + private AudioRecorderHandler audioRecorderHandler; + private final DataProcessor speakerConnectedDataProcessor; + private final DataProcessor speakerVolumeDataProcessor; + private final DataProcessor speakerStateDataProcessor; + private final DataProcessor isRecordingDataProcessor; + private final DataProcessor> audioMediaFilesDataProcessor; + private DJIKey speakerVolumeKey; + + private boolean isPersistFileEnabled; + private boolean isInstantPlayEnabled; + + + //endregion + + //region Lifecycle + public SpeakerControlWidgetModel(@NonNull DJISDKModel djiSdkModel, + @NonNull ObservableInMemoryKeyedStore keyedStore, + @NonNull AudioRecorderHandler audioRecorderHandler) { + super(djiSdkModel, keyedStore); + this.audioRecorderHandler = audioRecorderHandler; + speakerConnectedDataProcessor = DataProcessor.create(false); + isRecordingDataProcessor = DataProcessor.create(false); + speakerVolumeDataProcessor = DataProcessor.create(DEFAULT_VOLUME); + speakerStateDataProcessor = DataProcessor.create(new SpeakerState.Builder() + .index(0) + .playingMode(SettingsDefinitions.PlayMode.SINGLE_ONCE) + .playingState(SettingsDefinitions.SpeakerPlayingState.STOPPED) + .storageLocation(SettingsDefinitions.AudioStorageLocation.TEMPORARY) + .volume(DEFAULT_VOLUME) + .build()); + audioMediaFilesDataProcessor = DataProcessor.create(new ArrayList<>()); + + } + + @Override + protected void inSetup() { + DJIKey speakerConnectedKey = AccessoryAggregationKey.createSpeakerKey(AccessoryAggregationKey.CONNECTION); + bindDataProcessor(speakerConnectedKey, speakerConnectedDataProcessor, speakerConnected -> onSpeakerConnected((boolean) speakerConnected)); + speakerVolumeKey = AccessoryAggregationKey.createSpeakerKey(AccessoryAggregationKey.SPEAKER_VOLUME); + bindDataProcessor(speakerVolumeKey, speakerVolumeDataProcessor); + } + + + @Override + protected void inCleanup() { + // No clean up + } + + @Override + protected void updateStates() { + // no states + } + + //endregion + + //region Speaker state callback + @Override + public void onUpdate(SpeakerState state) { + speakerStateDataProcessor.onNext(state); + } + //endregion + + //region file list state callback + @Override + public void onFileListStateChange(MediaManager.FileListState state) { + getPlaylist(); + } + //endregion + + //region recording callback + + + @Override + public void onRecording(byte[] data) { + if (speaker != null) { + speaker.paceData(data); + } + + + } + + @Override + public void onStopRecord(String savedPath) { + if (speaker != null) { + speaker.markEOF(); + } + if (audioRecorderHandler != null) { + audioRecorderHandler.deleteLastRecordFile(); + } + } + + //endregion + + //region Upload file callback + @Override + public void onStart() { + startRecordUsingMic(); + } + + @Override + public void onProgress(int dataSize) { + } + + @Override + public void onFinish(int index) { + if (isInstantPlayEnabled) { + addDisposable(playFile(index).subscribe(() -> { + }, error -> DJILog.d(TAG, "PLAY FILE " + error))); + } + isRecordingDataProcessor.onNext(false); + + } + + @Override + public void onFailure(DJIError error) { + isRecordingDataProcessor.onNext(false); + } + //endregion + + //region actions + + /** + * Start recording the audio that the speaker should play + * + * @return Completable representing success or failure of action + */ + public Completable startRecording() { + return Completable.create(emitter -> { + if (speaker == null) { + emitter.onError(new UXSDKError(DJISDKError.COMMAND_EXECUTION_FAILED)); + } + Format format = new SimpleDateFormat("MMM dd HH:mm:ss", Locale.US); + AudioFileInfo uploadInfo = new AudioFileInfo("AUD" + format.format(Calendar.getInstance().getTime()), + SettingsDefinitions.AudioStorageLocation.TEMPORARY); + if (isPersistFileEnabled) { + uploadInfo.setStorageLocation(SettingsDefinitions.AudioStorageLocation.PERSISTENT); + } + speaker.startTransmission(uploadInfo, SpeakerControlWidgetModel.this); + emitter.onComplete(); + }); + } + + /** + * Stop recording the audio to complete recording + * + * @return Completable representing success or failure of action + */ + public Completable stopRecording() { + return Completable.create(emitter -> { + if (speaker == null) { + emitter.onError(new UXSDKError(DJISDKError.COMMAND_EXECUTION_FAILED)); + } + if (audioRecorderHandler == null) { + emitter.onError(new UXSDKError(DJISDKError.COMMAND_EXECUTION_FAILED)); + } + audioRecorderHandler.stopRecord(); + emitter.onComplete(); + }); + } + + /** + * Play file on speaker + * + * @param index of file that should be played + * @return Completable representing success or failure of action + */ + public Completable playFile(final int index) { + if (speakerStateDataProcessor.getValue().getPlayingState() == SettingsDefinitions.SpeakerPlayingState.PLAYING) { + + return Completable.create(emitter -> { + if (speaker == null) { + emitter.onError(new UXSDKError(DJISDKError.COMMAND_EXECUTION_FAILED)); + } + speaker.stop(error -> { + if (error == null) { + emitter.onComplete(); + + } else { + emitter.onError(new UXSDKError(error)); + } + }); + }).andThen(play(index)); + + } else { + return play(index); + } + } + + /** + * Delete file from speaker + * + * @param index of file that should be deleted + * @return Completable representing success or failure of action + */ + public Completable deleteOneFileByIndex(final int index) { + return Completable.create(emitter -> { + if (speaker == null || audioMediaFilesDataProcessor.getValue().size() < index) { + emitter.onError(new UXSDKError(DJISDKError.COMMAND_EXECUTION_FAILED)); + } + ArrayList fileToDelete = new ArrayList<>(); + fileToDelete.add(audioMediaFilesDataProcessor.getValue().get(index).getIndex()); + speaker.delete(fileToDelete, new CommonCallbacks.CompletionCallbackWithTwoParam, DJIError>() { + @Override + public void onSuccess(List x, DJIError y) { + emitter.onComplete(); + } + + @Override + public void onFailure(DJIError error) { + emitter.onError(new UXSDKError(error)); + } + }); + }); + + } + + /** + * Set play in loop enabled. + * This will play the same file on repeat + * + * @param isEnabled boolean true - loop mode false - not loop mode + * @return Completable representing success or failure of action + */ + public Completable setLoopModeEnabled(boolean isEnabled) { + return Completable.create(emitter -> { + if (speaker == null) { + emitter.onError(new UXSDKError(DJISDKError.COMMAND_EXECUTION_FAILED)); + } + SettingsDefinitions.PlayMode playMode = SettingsDefinitions.PlayMode.SINGLE_ONCE; + if (isEnabled) { + playMode = SettingsDefinitions.PlayMode.REPEAT_SINGLE; + } + speaker.setPlayMode(playMode, error -> { + if (error == null) { + emitter.onComplete(); + } else { + emitter.onError(new UXSDKError(error)); + } + }); + + }); + + } + + /** + * Set speaker volume + * + * @param volume integer value representing volume + * @return Completable representing success or failure of action + */ + public Completable setSpeakerVolume(@IntRange(from = 0, to = 100) int volume) { + return djiSdkModel.setValue(speakerVolumeKey, volume).subscribeOn(SchedulerProvider.io()); + } + + /** + * Set persist file enabled. + * If enabled the file will be saved on device to be played again later. + * If disabled the recorded file will be one time broadcast + * + * @param isPersistFileEnabled boolean true - enabled false - disabled + */ + public void setPersistFileEnabled(boolean isPersistFileEnabled) { + this.isPersistFileEnabled = isPersistFileEnabled; + } + + /** + * Set instant play enabled. + * If enabled the speaker will play immediately after the recording is complete + * If disabled the file will not be played immediately. + * + * @param isInstantPlayEnabled Completable representing success or failure of action + */ + public void setInstantPlayEnabled(boolean isInstantPlayEnabled) { + this.isInstantPlayEnabled = isInstantPlayEnabled; + } + + //endregion + + //region Data + + /** + * Get the speaker volume + * + * @return Flowable with integer value representing volume + */ + public Flowable getSpeakerVolume() { + return speakerVolumeDataProcessor.toFlowable(); + } + + /** + * Get the current speaker state + * + * @return Flowable with instance of {@link SpeakerState} + */ + public Flowable getSpeakerState() { + return speakerStateDataProcessor.toFlowable(); + } + + /** + * Get the list of audio files from the device + * + * @return Flowable with list of {@link AudioMediaFile} + */ + public Flowable> getMediaFileList() { + return audioMediaFilesDataProcessor.toFlowable(); + } + + /** + * Check if recording is in progress + * + * @return Flowable with boolean value true - recording false - not recording + */ + public Flowable isRecording() { + return isRecordingDataProcessor.toFlowable(); + } + //endregion + + //region private helpers + private Completable play(final int listPositionIndex) { + return Completable.create(emitter -> { + if (speaker == null) { + emitter.onError(new UXSDKError(DJISDKError.COMMAND_EXECUTION_FAILED)); + } + speaker.play(audioMediaFilesDataProcessor.getValue().get(listPositionIndex).getIndex(), error -> { + if (error == null) { + emitter.onComplete(); + } else { + emitter.onError(new UXSDKError(error)); + } + }); + }); + } + + private void getPlaylist() { + if (speaker != null) { + speaker.refreshFileList(error -> { + if (null == error) { + List audioMediaFiles = speaker.getFileListSnapshot(); + if (audioMediaFiles != null) { + audioMediaFilesDataProcessor.onNext(audioMediaFiles); + } + + } + }); + } + } + + private void onSpeakerConnected(boolean speakerConnected) { + if (speakerConnected) { + speaker = ProductUtil.getSpeaker(); + if (speaker != null) { + speaker.setStateCallback(this); + speaker.addFileListStateListener(this); + } + + } + + } + + private void startRecordUsingMic() { + if (audioRecorderHandler != null) { + audioRecorderHandler.startRecord(this); + isRecordingDataProcessor.onNext(true); + } + } + + //endregion +} diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/speaker/SpeakerIndicatorWidget.java b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/speaker/SpeakerIndicatorWidget.java new file mode 100644 index 00000000..181fde18 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/speaker/SpeakerIndicatorWidget.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2018-2020 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.speaker; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import dji.log.DJILog; +import dji.ux.beta.core.base.SchedulerProvider; +import dji.ux.beta.accessory.R; +import dji.ux.beta.accessory.widget.speaker.SpeakerIndicatorWidgetModel.SpeakerIndicatorState; +import dji.ux.beta.core.base.DJISDKModel; +import dji.ux.beta.core.base.UXSDKError; +import dji.ux.beta.core.base.widget.FrameLayoutWidget; +import dji.ux.beta.core.communication.ObservableInMemoryKeyedStore; +import dji.ux.beta.core.communication.OnStateChangeCallback; + +import static dji.ux.beta.core.extension.TypedArrayExtensions.INVALID_RESOURCE; + +/** + * Speaker Indicator Widget will display the state of the speaker accessory + *

+ * Widget is configured to show only when the accessory is connected + * When widget is visible it displays if the speaker is currently playing audio + *

+ * Tapping the widget can be used to open {@link SpeakerControlWidget} + */ +public class SpeakerIndicatorWidget extends FrameLayoutWidget implements View.OnClickListener { + + //region + private static final String TAG = "SpeakerIndicatorWidget"; + private SpeakerIndicatorWidgetModel widgetModel; + private Drawable speakerActiveIcon; + private Drawable speakerInactiveIcon; + private ImageView foregroundImageView; + private OnStateChangeCallback stateChangeCallback = null; + private int stateChangeResourceId; + + + //endregion + + //region Lifecycle + public SpeakerIndicatorWidget(@NonNull Context context) { + super(context); + } + + public SpeakerIndicatorWidget(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public SpeakerIndicatorWidget(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + inflate(context, R.layout.uxsdk_widget_speaker_indicator, this); + setBackgroundResource(R.drawable.uxsdk_background_black_rectangle); + foregroundImageView = findViewById(R.id.image_view_speaker_indicator); + + if (!isInEditMode()) { + widgetModel = + new SpeakerIndicatorWidgetModel(DJISDKModel.getInstance(), ObservableInMemoryKeyedStore.getInstance()); + } + initDefaults(); + stateChangeResourceId = INVALID_RESOURCE; + if (attrs != null) { + initAttributes(context, attrs); + } + } + + @Override + protected void reactToModelChanges() { + addReaction(widgetModel.getSpeakerIndicatorState().observeOn(SchedulerProvider.ui()).subscribe(this::updateUI)); + } + + + @NonNull + @Override + public String getIdealDimensionRatioString() { + return getResources().getString(R.string.uxsdk_widget_default_ratio); + } + + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!isInEditMode()) { + widgetModel.setup(); + } + initializeListener(); + + } + + @Override + protected void onDetachedFromWindow() { + if (!isInEditMode()) { + widgetModel.cleanup(); + } + super.onDetachedFromWindow(); + } + + + @Override + public void onClick(View v) { + if (stateChangeCallback != null) { + stateChangeCallback.onStateChange(null); + } + } + + //endregion + + //region private methods + + private void initializeListener() { + if (stateChangeResourceId != INVALID_RESOURCE && this.getRootView() != null) { + View widgetView = this.getRootView().findViewById(stateChangeResourceId); + if (widgetView instanceof SpeakerControlWidget) { + setStateChangeCallback((SpeakerControlWidget) widgetView); + } + } + } + + private void initDefaults() { + speakerActiveIcon = getResources().getDrawable(R.drawable.uxsdk_ic_speaker_active); + speakerInactiveIcon = getResources().getDrawable(R.drawable.uxsdk_ic_speaker_inactive); + } + + + private void initAttributes(Context context, AttributeSet attrs) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SpeakerIndicatorWidget); + stateChangeResourceId = + typedArray.getResourceId(R.styleable.SpeakerIndicatorWidget_uxsdk_onStateChange, INVALID_RESOURCE); + if (typedArray.getDrawable(R.styleable.SpeakerIndicatorWidget_uxsdk_speakerActiveDrawable) != null) { + speakerActiveIcon = typedArray.getDrawable(R.styleable.SpeakerIndicatorWidget_uxsdk_speakerActiveDrawable); + } + + if (typedArray.getDrawable(R.styleable.SpeakerIndicatorWidget_uxsdk_speakerInactiveDrawable) != null) { + speakerInactiveIcon = typedArray.getDrawable(R.styleable.SpeakerIndicatorWidget_uxsdk_speakerInactiveDrawable); + } + if (typedArray.getDrawable(R.styleable.SpeakerIndicatorWidget_uxsdk_iconBackground) != null) { + setIconBackground(typedArray.getDrawable(R.styleable.SpeakerIndicatorWidget_uxsdk_iconBackground)); + } + + typedArray.recycle(); + } + + private void updateUI(SpeakerIndicatorState speakerIndicatorState) { + if (speakerIndicatorState == SpeakerIndicatorState.HIDDEN) { + setVisibility(GONE); + } else { + setVisibility(VISIBLE); + if (speakerIndicatorState == SpeakerIndicatorState.ACTIVE) { + foregroundImageView.setImageDrawable(speakerActiveIcon); + } else if (speakerIndicatorState == SpeakerIndicatorState.INACTIVE) { + foregroundImageView.setImageDrawable(speakerInactiveIcon); + } + } + } + + private void checkAndUpdateUI() { + if (!isInEditMode()) { + addDisposable(widgetModel.getSpeakerIndicatorState().lastOrError() + .observeOn(SchedulerProvider.ui()) + .subscribe( + this::updateUI, + error -> { + if (error instanceof UXSDKError) { + DJILog.e(TAG, error.toString()); + } + })); + } + } + //endregion + + //region customizations + + /** + * Set call back for when the widget is tapped. + * This can be used to link the widget to {@link SpeakerControlWidget} + * + * @param stateChangeCallback listener to handle call backs + */ + public void setStateChangeCallback(@NonNull OnStateChangeCallback stateChangeCallback) { + this.stateChangeCallback = stateChangeCallback; + } + + /** + * Set speaker active state icon resource + * + * @param resourceId resource id of speaker active icon + */ + public void setSpeakerActiveIcon(@DrawableRes int resourceId) { + setSpeakerActiveIcon(getResources().getDrawable(resourceId)); + } + + /** + * Set speaker active state icon drawable + * + * @param drawable to be used as speaker active + */ + public void setSpeakerActiveIcon(@Nullable Drawable drawable) { + speakerActiveIcon = drawable; + checkAndUpdateUI(); + } + + /** + * Get speaker active state icon drawable + * + * @return Drawable + */ + @Nullable + public Drawable getSpeakerActiveIcon() { + return speakerActiveIcon; + } + + + /** + * Set speaker inactive state icon resource + * + * @param resourceId resource id of speaker inactive icon + */ + public void setSpeakerInactiveIcon(@DrawableRes int resourceId) { + setSpeakerInactiveIcon(getResources().getDrawable(resourceId)); + } + + /** + * Set speaker inactive state icon drawable + * + * @param drawable to be used as speaker inactive state + */ + public void setSpeakerInactiveIcon(@Nullable Drawable drawable) { + speakerInactiveIcon = drawable; + checkAndUpdateUI(); + } + + /** + * Get speaker inactive state icon + * + * @return Drawable + */ + @Nullable + public Drawable getSpeakerInactiveIcon() { + return speakerInactiveIcon; + } + + /** + * Set background to icon + * + * @param resourceId resource id of background + */ + public void setIconBackground(@DrawableRes int resourceId) { + setIconBackground(getResources().getDrawable(resourceId)); + } + + /** + * Set background to icon + * + * @param drawable to be used as background + */ + public void setIconBackground(@Nullable Drawable drawable) { + foregroundImageView.setBackground(drawable); + } + + /** + * Get current background of icon + * + * @return Drawable + */ + @Nullable + public Drawable getIconBackground() { + return foregroundImageView.getBackground(); + } + + //endregion +} diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/speaker/SpeakerIndicatorWidgetModel.java b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/speaker/SpeakerIndicatorWidgetModel.java new file mode 100644 index 00000000..e0cac1fc --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/speaker/SpeakerIndicatorWidgetModel.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2018-2020 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.speaker; + +import androidx.annotation.NonNull; + +import dji.common.accessory.SettingsDefinitions; +import dji.common.accessory.SpeakerState; +import dji.keysdk.AccessoryAggregationKey; +import dji.keysdk.DJIKey; +import dji.thirdparty.io.reactivex.Flowable; +import dji.ux.beta.core.base.DJISDKModel; +import dji.ux.beta.core.base.WidgetModel; +import dji.ux.beta.core.communication.ObservableInMemoryKeyedStore; +import dji.ux.beta.core.util.DataProcessor; + +/** + * Speaker Indicator Widget Model + *

+ * Widget Model for the {@link SpeakerIndicatorWidget} used to define the + * underlying logic and communication + */ +public class SpeakerIndicatorWidgetModel extends WidgetModel { + + //region private fields + private final DataProcessor speakerConnectedProcessor; + private final DataProcessor speakerStateProcessor; + private final DataProcessor speakerIndicatorStateProcessor; + //endregion + + //region Lifecycle + public SpeakerIndicatorWidgetModel(@NonNull DJISDKModel djiSdkModel, + @NonNull ObservableInMemoryKeyedStore keyedStore) { + super(djiSdkModel, keyedStore); + speakerConnectedProcessor = DataProcessor.create(false); + speakerStateProcessor = DataProcessor.create(new SpeakerState.Builder().index(0) + .playingState(SettingsDefinitions.SpeakerPlayingState.UNKNOWN) + .playingMode(SettingsDefinitions.PlayMode.UNKNOWN) + .storageLocation(SettingsDefinitions.AudioStorageLocation.UNKNOWN) + .volume(0) + .build()); + speakerIndicatorStateProcessor = DataProcessor.create(SpeakerIndicatorState.HIDDEN); + } + + @Override + protected void inSetup() { + DJIKey speakerConnectedKey = AccessoryAggregationKey.createSpeakerKey(AccessoryAggregationKey.CONNECTION); + bindDataProcessor(speakerConnectedKey, speakerConnectedProcessor); + DJIKey speakerActiveKey = AccessoryAggregationKey.createSpeakerKey(AccessoryAggregationKey.SPEAKER_STATE); + bindDataProcessor(speakerActiveKey, speakerStateProcessor); + } + + @Override + protected void inCleanup() { + // No clean up + } + + @Override + protected void updateStates() { + if (speakerConnectedProcessor.getValue()) { + if (speakerStateProcessor.getValue().getPlayingState() == SettingsDefinitions.SpeakerPlayingState.PLAYING) { + speakerIndicatorStateProcessor.onNext(SpeakerIndicatorState.ACTIVE); + } else { + speakerIndicatorStateProcessor.onNext(SpeakerIndicatorState.INACTIVE); + } + } else { + speakerIndicatorStateProcessor.onNext(SpeakerIndicatorState.HIDDEN); + } + + + } + //endregion + + //region Data + + /** + * Get the speaker indicator state + * + * @return Flowable with instance of {@link SpeakerIndicatorState} + */ + public Flowable getSpeakerIndicatorState() { + return speakerIndicatorStateProcessor.toFlowable(); + } + + //endregion + + /** + * Enum representing the state of the indicator widget + */ + public enum SpeakerIndicatorState { + /** + * Speaker is not connected + */ + HIDDEN, + + /** + * Speaker is connected and playing audio + */ + ACTIVE, + + /** + * Speaker is connected but not playing audio + */ + INACTIVE + } + +} diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightControlWidget.java b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightControlWidget.java new file mode 100644 index 00000000..f9387fc0 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightControlWidget.java @@ -0,0 +1,766 @@ +/* + * Copyright (c) 2018-2020 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.spotlight; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.Switch; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.Dimension; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.StyleRes; + +import dji.ux.beta.accessory.R; +import dji.ux.beta.core.base.DJISDKModel; +import dji.ux.beta.core.base.SchedulerProvider; +import dji.ux.beta.core.base.widget.ConstraintLayoutWidget; +import dji.ux.beta.core.communication.ObservableInMemoryKeyedStore; +import dji.ux.beta.core.communication.OnStateChangeCallback; + +import static dji.ux.beta.core.extension.TypedArrayExtensions.INVALID_RESOURCE; + +/** + * Widget can be used to control of state of Spotlight accessory + * Spotlight can be switched on and switched off and its + * brightness can be controlled. + */ +public class SpotlightControlWidget extends ConstraintLayoutWidget + implements OnSeekBarChangeListener, OnCheckedChangeListener, OnStateChangeCallback { + + //region Fields + private static final String TAG = "SpotlightControlWidget"; + private SpotlightControlWidgetModel widgetModel; + private TextView headerTextView; + private TextView brightnessLabelTextView; + private TextView enabledLabelTextView; + private TextView temperatureLabelTextView; + private TextView temperatureValueTextView; + private TextView warningMessageTextView; + private SeekBar brightnessSeekbar; + private Switch enableSwitch; + //endregion + + //region Lifecycle + public SpotlightControlWidget(Context context) { + super(context); + } + + public SpotlightControlWidget(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SpotlightControlWidget(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + inflate(context, R.layout.uxsdk_widget_spotlight_control, this); + setBackgroundResource(R.drawable.uxsdk_background_black_rectangle); + headerTextView = findViewById(R.id.spotlight_header_text); + enabledLabelTextView = findViewById(R.id.spotlight_enabled_label); + brightnessLabelTextView = findViewById(R.id.spotlight_brightness_label); + brightnessSeekbar = findViewById(R.id.spotlight_brightness_seekbar); + temperatureLabelTextView = findViewById(R.id.spotlight_temperature_label); + temperatureValueTextView = findViewById(R.id.spotlight_temperature_value); + enableSwitch = findViewById(R.id.spotlight_enabled_switch); + warningMessageTextView = findViewById(R.id.spotlight_warning); + + if (!isInEditMode()) { + widgetModel = + new SpotlightControlWidgetModel(DJISDKModel.getInstance(), + ObservableInMemoryKeyedStore.getInstance()); + } + + enableSwitch.setOnCheckedChangeListener(this); + brightnessSeekbar.setOnSeekBarChangeListener(this); + + if (attrs != null) { + initAttributes(context, attrs); + } + } + + @Override + protected void reactToModelChanges() { + addReaction(widgetModel.isSpotlightConnected() + .observeOn(SchedulerProvider.ui()) + .subscribe(this::updateEnabled)); + + addReaction(widgetModel.getSpotlightState() + .observeOn(SchedulerProvider.ui()) + .subscribe(this::updateUI)); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!isInEditMode()) { + widgetModel.setup(); + } + } + + @Override + protected void onDetachedFromWindow() { + if (!isInEditMode()) { + widgetModel.cleanup(); + } + super.onDetachedFromWindow(); + } + + @NonNull + @Override + public String getIdealDimensionRatioString() { + return getResources().getString(R.string.uxsdk_widget_spotlight_control_ratio); + } + + @Override + public void onStateChange(@Nullable Object state) { + toggleVisibility(); + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + handleCheckChanged(); + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) handleSeekbarChanged(progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Empty function + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Empty function + } + //endregion + + //region private helpers + + private void toggleVisibility() { + if (getVisibility() == VISIBLE) { + setVisibility(GONE); + } else { + setVisibility(VISIBLE); + } + } + + private void initAttributes(Context context, AttributeSet attrs) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SpotlightControlWidget); + + int textAppearance = + typedArray.getResourceId(R.styleable.SpotlightControlWidget_uxsdk_widgetTitleTextAppearance, INVALID_RESOURCE); + if (textAppearance != INVALID_RESOURCE) { + setHeaderTextAppearance(textAppearance); + } + setHeaderTextColor(typedArray.getColor(R.styleable.SpotlightControlWidget_uxsdk_widgetTitleTextColor, Color.WHITE)); + setHeaderTextBackground(typedArray.getDrawable(R.styleable.SpotlightControlWidget_uxsdk_widgetTitleBackground)); + setHeaderTextSize(typedArray.getDimension(R.styleable.SpotlightControlWidget_uxsdk_widgetTitleTextSize, 14)); + textAppearance = typedArray.getResourceId(R.styleable.SpotlightControlWidget_uxsdk_labelsTextAppearance, INVALID_RESOURCE); + if (textAppearance != INVALID_RESOURCE) { + setLabelsTextAppearance(textAppearance); + } + setLabelsTextColor(typedArray.getColor(R.styleable.SpotlightControlWidget_uxsdk_labelsTextColor, Color.WHITE)); + setLabelsTextBackground(typedArray.getDrawable(R.styleable.SpotlightControlWidget_uxsdk_labelsBackground)); + setLabelsTextSize(typedArray.getDimension(R.styleable.SpotlightControlWidget_uxsdk_labelsTextSize, 12)); + textAppearance = typedArray.getResourceId(R.styleable.SpotlightControlWidget_uxsdk_warningTextAppearance, INVALID_RESOURCE); + if (textAppearance != INVALID_RESOURCE) { + setWarningMessageTextAppearance(textAppearance); + } + setWarningMessageTextColor(typedArray.getColor(R.styleable.SpotlightControlWidget_uxsdk_warningTextColor, Color.WHITE)); + setWarningMessageTextBackground(typedArray.getDrawable(R.styleable.SpotlightControlWidget_uxsdk_warningTextBackground)); + setWarningMessageTextSize(typedArray.getDimension(R.styleable.SpotlightControlWidget_uxsdk_warningTextSize, 12)); + textAppearance = + typedArray.getResourceId(R.styleable.SpotlightControlWidget_uxsdk_temperatureValueTextAppearance, INVALID_RESOURCE); + if (textAppearance != INVALID_RESOURCE) { + setTemperatureValueTextAppearance(textAppearance); + } + setTemperatureValueTextColor(typedArray.getColor(R.styleable.SpotlightControlWidget_uxsdk_temperatureValueTextColor, + Color.WHITE)); + setTemperatureValueTextBackground(typedArray.getDrawable(R.styleable.SpotlightControlWidget_uxsdk_temperatureValueTextBackground)); + setTemperatureValueTextSize(typedArray.getDimension(R.styleable.SpotlightControlWidget_uxsdk_temperatureValueTextSize, 12)); + + if (typedArray.getDrawable(R.styleable.SpotlightControlWidget_uxsdk_switchThumbIcon) != null) { + setSwitchThumb(typedArray.getDrawable(R.styleable.SpotlightControlWidget_uxsdk_switchThumbIcon)); + } + + if (typedArray.getDrawable(R.styleable.SpotlightControlWidget_uxsdk_seekbarThumbIcon) != null) { + setSwitchThumb(typedArray.getDrawable(R.styleable.SpotlightControlWidget_uxsdk_seekbarThumbIcon)); + } + + typedArray.recycle(); + } + + private void updateUI(SpotlightState spotlightState) { + enableSwitch.setChecked(spotlightState.isEnabled()); + temperatureValueTextView.setText(getResources().getString(R.string.uxsdk_spotlight_temperature_value, + spotlightState.getTemperature())); + brightnessSeekbar.setProgress(spotlightState.getBrightnessPercentage()); + } + + private void updateEnabled(boolean isConnected) { + brightnessSeekbar.setEnabled(isConnected); + enableSwitch.setEnabled(isConnected); + } + + private void handleCheckChanged() { + addDisposable(widgetModel.toggleSpotlight().subscribe(() -> { + }, logErrorConsumer(TAG, "Enable check "))); + } + + private void handleSeekbarChanged(int progress) { + addDisposable(widgetModel.setSpotlightBrightnessPercentage(progress).subscribe(() -> { + }, logErrorConsumer(TAG, "Set Progress "))); + } + + //endregion + + //region customizations + + /** + * Get the background of the header + * + * @return Drawable + */ + @Nullable + public Drawable getHeaderTextBackground() { + return headerTextView.getBackground(); + } + + /** + * Set the background of the header + * + * @param resourceId to be used + */ + public void setHeaderTextBackground(@DrawableRes int resourceId) { + headerTextView.setBackgroundResource(resourceId); + } + + /** + * Set the background of the header + * + * @param drawable to be used + */ + public void setHeaderTextBackground(@NonNull Drawable drawable) { + headerTextView.setBackground(drawable); + } + + /** + * Get the color state list of header text + * + * @return ColorStateList + */ + @Nullable + public ColorStateList getHeaderTextColors() { + return headerTextView.getTextColors(); + } + + /** + * Set color state list of header text + * + * @param colorStateList to be used + */ + public void setHeaderTextColors(@Nullable ColorStateList colorStateList) { + headerTextView.setTextColor(colorStateList); + } + + /** + * Get the color of header text + * + * @return integer value representing color + */ + @ColorInt + public int getHeaderTextColor() { + return headerTextView.getCurrentTextColor(); + } + + /** + * Set the color of header text + * + * @param color integer value + */ + public void setHeaderTextColor(@ColorInt int color) { + headerTextView.setTextColor(color); + } + + /** + * Get the current text size of header + * + * @return float value representing text size + */ + @Dimension + public float getHeaderTextSize() { + return headerTextView.getTextSize(); + } + + /** + * Set the text size of header + * + * @param textSize float value + */ + public void setHeaderTextSize(@Dimension float textSize) { + headerTextView.setTextSize(textSize); + } + + /** + * Set the text appearance of header + * + * @param textAppearance to be used + */ + public void setHeaderTextAppearance(@StyleRes int textAppearance) { + headerTextView.setTextAppearance(getContext(), textAppearance); + } + + /** + * Get the background of labels + * + * @return Drawable + */ + @Nullable + public Drawable getLabelsTextBackground() { + return brightnessLabelTextView.getBackground(); + } + + /** + * Set the background of labels + * + * @param resourceId to be used + */ + public void setLabelsTextBackground(@DrawableRes int resourceId) { + brightnessLabelTextView.setBackgroundResource(resourceId); + temperatureLabelTextView.setBackgroundResource(resourceId); + enabledLabelTextView.setBackgroundResource(resourceId); + } + + /** + * Set the background of labels + * + * @param drawable to be used + */ + public void setLabelsTextBackground(@NonNull Drawable drawable) { + brightnessLabelTextView.setBackground(drawable); + temperatureLabelTextView.setBackground(drawable); + enabledLabelTextView.setBackground(drawable); + } + + /** + * Get the color state list used for labels + * + * @return ColorStateList + */ + @Nullable + public ColorStateList getLabelsTextColors() { + return brightnessLabelTextView.getTextColors(); + } + + /** + * Set the color state list of label text + * + * @param colorStateList to be used + */ + public void setLabelsTextColors(@Nullable ColorStateList colorStateList) { + brightnessLabelTextView.setTextColor(colorStateList); + temperatureLabelTextView.setTextColor(colorStateList); + enabledLabelTextView.setTextColor(colorStateList); + } + + /** + * Get the current color of label text + * + * @return integer value representing color + */ + @ColorInt + public int getLabelsTextColor() { + return brightnessLabelTextView.getCurrentTextColor(); + } + + /** + * Set the color of text in all labels + * + * @param color integer value + */ + public void setLabelsTextColor(@ColorInt int color) { + brightnessLabelTextView.setTextColor(color); + temperatureLabelTextView.setTextColor(color); + enabledLabelTextView.setTextColor(color); + } + + /** + * Get the current text size of labels + * + * @return float value representing text size + */ + @Dimension + public float getLabelsTextSize() { + return brightnessLabelTextView.getTextSize(); + } + + /** + * Set the text size of all the labels + * + * @param textSize float value + */ + public void setLabelsTextSize(@Dimension float textSize) { + brightnessLabelTextView.setTextSize(textSize); + temperatureLabelTextView.setTextSize(textSize); + enabledLabelTextView.setTextSize(textSize); + } + + /** + * Set the text appearance of all the labels + * + * @param textAppearance to be used + */ + public void setLabelsTextAppearance(@StyleRes int textAppearance) { + brightnessLabelTextView.setTextAppearance(getContext(), textAppearance); + temperatureLabelTextView.setTextAppearance(getContext(), textAppearance); + enabledLabelTextView.setTextAppearance(getContext(), textAppearance); + } + + /** + * Get the background of the temperature value text + * + * @return Drawable + */ + @Nullable + public Drawable getTemperatureValueTextBackground() { + return temperatureValueTextView.getBackground(); + } + + /** + * Set the background of the temperature value text + * + * @param resourceId to be used + */ + public void setTemperatureValueTextBackground(@DrawableRes int resourceId) { + temperatureValueTextView.setBackgroundResource(resourceId); + } + + /** + * Set the background of the temperature value text + * + * @param drawable to be used + */ + public void setTemperatureValueTextBackground(@NonNull Drawable drawable) { + temperatureValueTextView.setBackground(drawable); + } + + /** + * Get the color state list used for temperature value text + * + * @return ColorStateList + */ + @Nullable + public ColorStateList getTemperatureValueTextColors() { + return temperatureValueTextView.getTextColors(); + } + + /** + * Set the color state list for temperature value text + * + * @param colorStateList to be used + */ + public void setTemperatureValueTextColors(@Nullable ColorStateList colorStateList) { + temperatureValueTextView.setTextColor(colorStateList); + } + + /** + * Get the current color of temperature text value + * + * @return integer value representing color + */ + @ColorInt + public int getTemperatureValueTextColor() { + return temperatureValueTextView.getCurrentTextColor(); + } + + /** + * Set the color of temperature value text + * + * @param color integer value + */ + public void setTemperatureValueTextColor(@ColorInt int color) { + temperatureValueTextView.setTextColor(color); + } + + /** + * Get the temperature value text size + * + * @return float value representing text size + */ + @Dimension + public float getTemperatureValueTextSize() { + return temperatureValueTextView.getTextSize(); + } + + /** + * Set the temperature value text size + * + * @param textSize float value + */ + public void setTemperatureValueTextSize(@Dimension float textSize) { + temperatureValueTextView.setTextSize(textSize); + } + + /** + * Set the text appearance for temperature value text + * + * @param textAppearance to be used + */ + public void setTemperatureValueTextAppearance(@StyleRes int textAppearance) { + temperatureValueTextView.setTextAppearance(getContext(), textAppearance); + } + + /** + * Get the current background of warning message text + * + * @return Drawable + */ + @Nullable + public Drawable getWarningMessageTextBackground() { + return warningMessageTextView.getBackground(); + } + + /** + * Set the background of warning message text + * + * @param resourceId to be used + */ + public void setWarningMessageTextBackground(@DrawableRes int resourceId) { + warningMessageTextView.setBackgroundResource(resourceId); + } + + /** + * Set the background of warning message text + * + * @param drawable to be used + */ + public void setWarningMessageTextBackground(@NonNull Drawable drawable) { + warningMessageTextView.setBackground(drawable); + } + + /** + * Get warning message text color state list + * + * @return ColorStateList + */ + @Nullable + public ColorStateList getWarningMessageTextColors() { + return warningMessageTextView.getTextColors(); + } + + /** + * Set the text color state list for warning message text + * + * @param colorStateList to be used + */ + public void setWarningMessageTextColors(@Nullable ColorStateList colorStateList) { + warningMessageTextView.setTextColor(colorStateList); + } + + /** + * Get the current text color of warning message + * + * @return integer value representing color + */ + @ColorInt + public int getWarningMessageTextColor() { + return warningMessageTextView.getCurrentTextColor(); + } + + /** + * Set the text color of warning text message + * + * @param color integer value + */ + public void setWarningMessageTextColor(@ColorInt int color) { + warningMessageTextView.setTextColor(color); + } + + /** + * Get the current text size of warning message + * + * @return float value representing text size + */ + @Dimension + public float getWarningMessageTextSize() { + return warningMessageTextView.getTextSize(); + } + + /** + * Set the text size of the warning message + * + * @param textSize float value + */ + public void setWarningMessageTextSize(@Dimension float textSize) { + warningMessageTextView.setTextSize(textSize); + } + + /** + * Set text appearance of message text + * + * @param textAppearance to be used + */ + public void setWarningMessageTextAppearance(@StyleRes int textAppearance) { + warningMessageTextView.setTextAppearance(getContext(), textAppearance); + } + + /** + * Get the thumb icon for the enable switch + * + * @return Drawable + */ + @Nullable + public Drawable getSwitchThumb() { + return enableSwitch.getThumbDrawable(); + } + + /** + * Set the thumb icon for the enable switch + * + * @param resourceId to be used + */ + public void setSwitchThumb(@DrawableRes int resourceId) { + enableSwitch.setThumbResource(resourceId); + } + + /** + * Set the thumb icon for the enable switch + * + * @param drawable to be used + */ + public void setSwitchThumb(@Nullable Drawable drawable) { + enableSwitch.setThumbDrawable(drawable); + } + + /** + * Get the thumb tint color state list for enable switch + * + * @return ColorStateList + */ + @RequiresApi(Build.VERSION_CODES.M) + @Nullable + public ColorStateList getSwitchThumbTintList() { + return enableSwitch.getThumbTintList(); + } + + /** + * Set the thumb tint color state list for enable switch + * + * @param colorStateList to be used + */ + @RequiresApi(Build.VERSION_CODES.M) + public void setSwitchThumbTintList(@Nullable ColorStateList colorStateList) { + enableSwitch.setThumbTintList(colorStateList); + } + + /** + * Get the thumb icon of brightness seek bar + * + * @return Drawable + */ + @Nullable + public Drawable getBrightnessSeekbarThumbDrawable() { + return brightnessSeekbar.getThumb(); + } + + /** + * Set the thumb icon for brightness seek bar + * + * @param resourceId to be used + */ + public void setBrightnessSeekbarThumbDrawable(@DrawableRes int resourceId) { + setBrightnessSeekbarThumbDrawable(getResources().getDrawable(resourceId)); + } + + /** + * Set the thumb icon for brightness seek bar + * + * @param drawable to be used + */ + public void setBrightnessSeekbarThumbDrawable(@NonNull Drawable drawable) { + brightnessSeekbar.setThumb(drawable); + } + + /** + * Get the color state list of the background of brightness seek bar + * + * @return ColorStateList + */ + @Nullable + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public ColorStateList getBrightnessSeekbarBackgroundTintList() { + return brightnessSeekbar.getBackgroundTintList(); + } + + /** + * Set the color state list for background of brightness seek bar + * + * @param colorStateList to be used + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public void setBrightnessSeekbarBackgroundTintList(@Nullable ColorStateList colorStateList) { + brightnessSeekbar.setBackgroundTintList(colorStateList); + } + + /** + * Get the color state list used for brightness seek bar progress tint + * + * @return ColorStateList + */ + @Nullable + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public ColorStateList getBrightnessSeekbarProgressTintList() { + return brightnessSeekbar.getProgressTintList(); + } + + /** + * Set the color state list for brightness seek bar progress tint + * + * @param colorStateList to be used + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public void setBrightnessSeekbarProgressTintList(@Nullable ColorStateList colorStateList) { + brightnessSeekbar.setProgressTintList(colorStateList); + } + //endregion +} diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightControlWidgetModel.java b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightControlWidgetModel.java new file mode 100644 index 00000000..ce257e69 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightControlWidgetModel.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2018-2020 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.spotlight; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; + +import dji.keysdk.AccessoryAggregationKey; +import dji.keysdk.DJIKey; +import dji.thirdparty.io.reactivex.Completable; +import dji.thirdparty.io.reactivex.Flowable; +import dji.ux.beta.core.base.DJISDKModel; +import dji.ux.beta.core.base.WidgetModel; +import dji.ux.beta.core.communication.ObservableInMemoryKeyedStore; +import dji.ux.beta.core.util.DataProcessor; + +/** + * Spotlight Control Widget Model + *

+ * Widget Model for the {@link SpotlightControlWidget} used to define the + * underlying logic and communication + */ +public class SpotlightControlWidgetModel extends WidgetModel { + + //region Fields + private static final int DEFAULT_BRIGHTNESS_PERCENTAGE = 0; + private static final float DEFAULT_TEMPERATURE = 0.0f; + private final DataProcessor spotlightConnectedDataProcessor; + private final DataProcessor spotlightEnabledDataProcessor; + private final DataProcessor spotlightBrightnessPercentageDataProcessor; + private final DataProcessor spotlightTemperatureDataProcessor; + private final DataProcessor spotlightStateProcessor; + private DJIKey spotlightEnabledKey; + private DJIKey spotlightBrightnessPercentageKey; + + //endregion + + + //region Lifecycle + public SpotlightControlWidgetModel(@NonNull DJISDKModel djiSdkModel, + @NonNull ObservableInMemoryKeyedStore keyedStore) { + super(djiSdkModel, keyedStore); + spotlightConnectedDataProcessor = DataProcessor.create(false); + spotlightEnabledDataProcessor = DataProcessor.create(false); + spotlightBrightnessPercentageDataProcessor = DataProcessor.create(0); + spotlightTemperatureDataProcessor = DataProcessor.create(0.0f); + spotlightStateProcessor = DataProcessor.create(new SpotlightState(false, DEFAULT_BRIGHTNESS_PERCENTAGE, DEFAULT_TEMPERATURE)); + } + + @Override + protected void inSetup() { + DJIKey spotlightConnectedKey = AccessoryAggregationKey.createSpotlightKey(AccessoryAggregationKey.CONNECTION); + bindDataProcessor(spotlightConnectedKey, spotlightConnectedDataProcessor); + spotlightEnabledKey = AccessoryAggregationKey.createSpotlightKey(AccessoryAggregationKey.SPOTLIGHT_ENABLED); + bindDataProcessor(spotlightEnabledKey, spotlightEnabledDataProcessor); + spotlightBrightnessPercentageKey = AccessoryAggregationKey.createSpotlightKey(AccessoryAggregationKey.SPOTLIGHT_BRIGHTNESS); + bindDataProcessor(spotlightBrightnessPercentageKey, spotlightBrightnessPercentageDataProcessor); + DJIKey spotlightTemperatureKey = AccessoryAggregationKey.createSpotlightKey(AccessoryAggregationKey.SPOTLIGHT_TEMPERATURE); + bindDataProcessor(spotlightTemperatureKey, spotlightTemperatureDataProcessor); + } + + @Override + protected void inCleanup() { + // Empty Function + } + + @Override + protected void updateStates() { + int percentage = spotlightEnabledDataProcessor.getValue() ? spotlightBrightnessPercentageDataProcessor.getValue() : DEFAULT_BRIGHTNESS_PERCENTAGE; + float temperature = spotlightEnabledDataProcessor.getValue() ? spotlightTemperatureDataProcessor.getValue() : DEFAULT_TEMPERATURE; + spotlightStateProcessor.onNext(new SpotlightState(spotlightEnabledDataProcessor.getValue(), percentage, temperature)); + } + + //endregion + + //region Data + + /** + * Check if spotlight is connected + * + * @return Flowable with boolean value true - connected false - not connected + */ + public Flowable isSpotlightConnected() { + return spotlightConnectedDataProcessor.toFlowable(); + } + + /** + * Get the current state of the spotlight + * + * @return Flowable with {@link SpotlightState} + */ + public Flowable getSpotlightState() { + return spotlightStateProcessor.toFlowable(); + } + //endregion + + //region + + /** + * Switch the spotlight ON and OFF + * + * @return Completable representing the state of the action + */ + public Completable toggleSpotlight() { + return djiSdkModel.setValue(spotlightEnabledKey, !spotlightEnabledDataProcessor.getValue()); + } + + /** + * Set the brightness of the spotlight + * + * @param value integer value of brightness percentage + * @return Completable representing the state of the action + */ + public Completable setSpotlightBrightnessPercentage(@IntRange(from = 0, to = 100) int value) { + return djiSdkModel.setValue(spotlightBrightnessPercentageKey, value); + } + //endregion +} diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightIndicatorWidget.java b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightIndicatorWidget.java new file mode 100644 index 00000000..7b5b0332 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightIndicatorWidget.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2018-2020 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.spotlight; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageView; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import dji.log.DJILog; +import dji.ux.beta.core.base.SchedulerProvider; +import dji.ux.beta.accessory.R; +import dji.ux.beta.accessory.widget.spotlight.SpotlightIndicatorWidgetModel.SpotlightIndicatorState; +import dji.ux.beta.core.base.DJISDKModel; +import dji.ux.beta.core.base.UXSDKError; +import dji.ux.beta.core.base.widget.FrameLayoutWidget; +import dji.ux.beta.core.communication.ObservableInMemoryKeyedStore; +import dji.ux.beta.core.communication.OnStateChangeCallback; + +import static dji.ux.beta.core.extension.TypedArrayExtensions.INVALID_RESOURCE; + +/** + * Widget represents the state of Spotlight accessory + * Widget is configured to show only when the accessory is connected + * Tapping on the widget can be used to open the {@link SpotlightControlWidget} + */ +public class SpotlightIndicatorWidget extends FrameLayoutWidget implements OnClickListener { + + //region Fields + private static final String TAG = "SpotlightIndWidget"; + private SpotlightIndicatorWidgetModel widgetModel; + private ImageView foregroundImageView; + private Drawable spotlightEnabledIcon; + private Drawable spotlightDisabledIcon; + private OnStateChangeCallback stateChangeCallback = null; + private int stateChangeResourceId; + //endregion + + //region Lifecycle + public SpotlightIndicatorWidget(@NonNull Context context) { + super(context); + } + + public SpotlightIndicatorWidget(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public SpotlightIndicatorWidget(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + inflate(context, R.layout.uxsdk_widget_spotlight_indicator, this); + setBackgroundResource(R.drawable.uxsdk_background_black_rectangle); + foregroundImageView = findViewById(R.id.image_view_spotlight_indicator); + setOnClickListener(this); + + if (!isInEditMode()) { + widgetModel = new SpotlightIndicatorWidgetModel(DJISDKModel.getInstance(), + ObservableInMemoryKeyedStore.getInstance()); + } + initDefaults(); + stateChangeResourceId = INVALID_RESOURCE; + if (attrs != null) { + initAttributes(context, attrs); + } + } + + @Override + public void onClick(View v) { + if (stateChangeCallback != null) { + stateChangeCallback.onStateChange(null); + } + } + + + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!isInEditMode()) { + widgetModel.setup(); + } + initializeListener(); + } + + @Override + protected void onDetachedFromWindow() { + destroyListener(); + if (!isInEditMode()) { + widgetModel.cleanup(); + } + super.onDetachedFromWindow(); + } + + @Override + protected void reactToModelChanges() { + addReaction(widgetModel.getSpotlightState() + .observeOn(SchedulerProvider.ui()) + .subscribe(this::updateUI)); + } + + @NonNull + @Override + public String getIdealDimensionRatioString() { + return getResources().getString(R.string.uxsdk_widget_default_ratio); + } + //endregion + + //region private methods + private void initializeListener() { + if (stateChangeResourceId != INVALID_RESOURCE && this.getRootView() != null) { + View widgetView = this.getRootView().findViewById(stateChangeResourceId); + if (widgetView instanceof SpotlightControlWidget) { + setStateChangeCallback((SpotlightControlWidget) widgetView); + } + } + } + + private void destroyListener() { + stateChangeCallback = null; + } + + private void initDefaults() { + spotlightEnabledIcon = getResources().getDrawable(R.drawable.uxsdk_ic_spotlight_enabled); + spotlightDisabledIcon = getResources().getDrawable(R.drawable.uxsdk_ic_spotlight_disabled); + } + + private void initAttributes(Context context, AttributeSet attrs) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SpotlightIndicatorWidget); + stateChangeResourceId = + typedArray.getResourceId(R.styleable.SpotlightIndicatorWidget_uxsdk_onStateChange, INVALID_RESOURCE); + + if (typedArray.getDrawable(R.styleable.SpotlightIndicatorWidget_uxsdk_spotlightEnabled) != null) { + spotlightEnabledIcon = typedArray.getDrawable(R.styleable.SpotlightIndicatorWidget_uxsdk_spotlightEnabled); + } + + if (typedArray.getDrawable(R.styleable.SpotlightIndicatorWidget_uxsdk_spotlightDisabled) != null) { + spotlightDisabledIcon = typedArray.getDrawable(R.styleable.SpotlightIndicatorWidget_uxsdk_spotlightDisabled); + } + setIconBackground(typedArray.getDrawable(R.styleable.SpotlightIndicatorWidget_uxsdk_iconBackground)); + + typedArray.recycle(); + } + + private void updateUI(SpotlightIndicatorState spotlightState) { + if (spotlightState == SpotlightIndicatorState.HIDDEN) { + setVisibility(GONE); + } else { + setVisibility(VISIBLE); + if (spotlightState == SpotlightIndicatorState.ACTIVE) { + foregroundImageView.setImageDrawable(spotlightEnabledIcon); + } else if (spotlightState == SpotlightIndicatorState.INACTIVE) { + foregroundImageView.setImageDrawable(spotlightDisabledIcon); + } + } + } + + private void checkAndUpdateUI() { + if (!isInEditMode()) { + addDisposable(widgetModel.getSpotlightState() + .lastOrError() + .observeOn(SchedulerProvider.ui()) + .subscribe(this::updateUI, error -> { + if (error instanceof UXSDKError) { + DJILog.e(TAG, error.toString()); + } + })); + } + } + + //endregion + + /** + * Set call back for when the widget is tapped. + * This can be used to link the widget to {@link SpotlightControlWidget} + * + * @param stateChangeCallback listener to handle call backs + */ + public void setStateChangeCallback(@NonNull OnStateChangeCallback stateChangeCallback) { + this.stateChangeCallback = stateChangeCallback; + } + + //region customizations + + /** + * Set spotlight enabled resource id + * + * @param resourceId resource id of spotlight enabled + */ + public void setSpotlightEnabledIcon(@DrawableRes int resourceId) { + setSpotlightEnabledIcon(getResources().getDrawable(resourceId)); + } + + + /** + * Set spotlight enabled drawable + * + * @param drawable Object to be used as spotlight enabled + */ + public void setSpotlightEnabledIcon(@Nullable Drawable drawable) { + spotlightEnabledIcon = drawable; + checkAndUpdateUI(); + } + + + /** + * Get spotlight enabled icon drawable + */ + @Nullable + public Drawable getSpotlightEnabledIcon() { + return spotlightEnabledIcon; + } + + + /** + * Set spotlight disabled resource + * + * @param resourceId resource id of spotlight disabled + */ + public void setSpotlightDisabledIcon(@DrawableRes int resourceId) { + setSpotlightDisabledIcon(getResources().getDrawable(resourceId)); + } + + + /** + * Set spotlight disabled drawable + * + * @param drawable Object to be used as spotlight disabled icon + */ + public void setSpotlightDisabledIcon(@Nullable Drawable drawable) { + spotlightDisabledIcon = drawable; + checkAndUpdateUI(); + } + + + /** + * Get spotlight disabled icon + */ + @Nullable + public Drawable getSpotlightDisabledIcon() { + return spotlightDisabledIcon; + } + + /** + * Set background to icon + * + * @param resourceId resource id of background + */ + public void setIconBackground(@DrawableRes int resourceId) { + setIconBackground(getResources().getDrawable(resourceId)); + } + + /** + * Set background to icon + * + * @param drawable Object to be used as background + */ + public void setIconBackground(@Nullable Drawable drawable) { + foregroundImageView.setBackground(drawable); + } + + /** + * Get current background of icon + */ + @Nullable + public Drawable getIconBackground() { + return foregroundImageView.getBackground(); + } + + //endregion +} + diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightIndicatorWidgetModel.java b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightIndicatorWidgetModel.java new file mode 100644 index 00000000..d371a164 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightIndicatorWidgetModel.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2018-2020 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.spotlight; + +import androidx.annotation.NonNull; + +import dji.keysdk.AccessoryAggregationKey; +import dji.keysdk.DJIKey; +import dji.thirdparty.io.reactivex.Flowable; +import dji.ux.beta.core.base.DJISDKModel; +import dji.ux.beta.core.base.WidgetModel; +import dji.ux.beta.core.communication.ObservableInMemoryKeyedStore; +import dji.ux.beta.core.util.DataProcessor; + +/** + * Spotlight Indicator Widget Model + *

+ * Widget Model for the {@link SpotlightIndicatorWidget} used to define the + * underlying logic and communication + */ +public class SpotlightIndicatorWidgetModel extends WidgetModel { + + //region + private final DataProcessor spotlightConnectedDataProcessor; + private final DataProcessor spotlightEnabledDataProcessor; + private final DataProcessor spotlightStateDataProcessor; + + //endregion + + // region Lifecycle + public SpotlightIndicatorWidgetModel(@NonNull DJISDKModel djiSdkModel, + @NonNull ObservableInMemoryKeyedStore keyedStore) { + super(djiSdkModel, keyedStore); + spotlightConnectedDataProcessor = DataProcessor.create(false); + spotlightEnabledDataProcessor = DataProcessor.create(false); + spotlightStateDataProcessor = DataProcessor.create(SpotlightIndicatorState.HIDDEN); + + } + + @Override + protected void inSetup() { + DJIKey spotlightConnectedKey = AccessoryAggregationKey.createSpotlightKey(AccessoryAggregationKey.CONNECTION); + bindDataProcessor(spotlightConnectedKey, spotlightConnectedDataProcessor); + DJIKey spotlightEnabledKey = AccessoryAggregationKey.createSpotlightKey(AccessoryAggregationKey.SPOTLIGHT_ENABLED); + bindDataProcessor(spotlightEnabledKey, spotlightEnabledDataProcessor); + } + + @Override + protected void inCleanup() { + // No clean up required + } + + @Override + protected void updateStates() { + if (spotlightConnectedDataProcessor.getValue()) { + if (spotlightEnabledDataProcessor.getValue()) { + spotlightStateDataProcessor.onNext(SpotlightIndicatorState.ACTIVE); + } else { + spotlightStateDataProcessor.onNext(SpotlightIndicatorState.INACTIVE); + } + } else { + spotlightStateDataProcessor.onNext(SpotlightIndicatorState.HIDDEN); + } + } + //endregion + + //region Data + + /** + * Get the current state that the indicator should display + * + * @return Flowable with instance of {@link SpotlightIndicatorState} + */ + public Flowable getSpotlightState() { + return spotlightStateDataProcessor.toFlowable(); + } + + //endregion + + /** + * Enum representing the state of the indicator widget + */ + public enum SpotlightIndicatorState { + /** + * Spotlight is not connected + */ + HIDDEN, + /** + * Spotlight is currently connected and switched on + */ + ACTIVE, + /** + * Spotlight is currently connected and switched off + */ + INACTIVE + } + +} diff --git a/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightState.java b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightState.java new file mode 100644 index 00000000..6f32dc97 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/java/dji/ux/beta/accessory/widget/spotlight/SpotlightState.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2018-2020 DJI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dji.ux.beta.accessory.widget.spotlight; + +/** + * Class represents the state of the spotlight accessory + */ +public class SpotlightState { + + private boolean isEnabled; + private float temperature; + private int brightnessPercentage; + + public SpotlightState(boolean isEnabled, int brightnessPercentage, float temperature) { + this.isEnabled = isEnabled; + this.temperature = temperature; + this.brightnessPercentage = brightnessPercentage; + } + + /** + * Check if the spotlight currently switched on + * + * @return boolean value true - spotlight on false - spotlight off + */ + public boolean isEnabled() { + return isEnabled; + } + + /** + * Get the temperature of the spotlight + * + * @return float value representing temperature + */ + public float getTemperature() { + return temperature; + } + + /** + * Get the brightness percentage + * + * @return integer value representing percentage + */ + public int getBrightnessPercentage() { + return brightnessPercentage; + } + + @Override + public int hashCode() { + int result = 31 * Float.floatToIntBits(temperature); + result = result + 31 * Float.floatToIntBits(brightnessPercentage); + result = result + 31 * (isEnabled ? 1 : 0); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SpotlightState)) return false; + SpotlightState that = (SpotlightState) o; + return isEnabled == that.isEnabled && + Float.compare(that.temperature, temperature) == 0 && + brightnessPercentage == that.brightnessPercentage; + } +} diff --git a/android-uxsdk-beta-accessory/src/main/res/color/uxsdk_selector_speaker_tab_colors.xml b/android-uxsdk-beta-accessory/src/main/res/color/uxsdk_selector_speaker_tab_colors.xml new file mode 100644 index 00000000..2397a750 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/color/uxsdk_selector_speaker_tab_colors.xml @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_beacon_active.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_beacon_active.xml new file mode 100644 index 00000000..fa439df8 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_beacon_active.xml @@ -0,0 +1,53 @@ + + + + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_beacon_inactive.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_beacon_inactive.xml new file mode 100644 index 00000000..52eff8ed --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_beacon_inactive.xml @@ -0,0 +1,53 @@ + + + + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_record_button_disabled.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_record_button_disabled.xml new file mode 100644 index 00000000..da6fe9c9 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_record_button_disabled.xml @@ -0,0 +1,57 @@ + + + + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_record_button_enabled.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_record_button_enabled.xml new file mode 100644 index 00000000..65bf99ad --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_record_button_enabled.xml @@ -0,0 +1,55 @@ + + + + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_active.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_active.xml new file mode 100644 index 00000000..3bcaa138 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_active.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_inactive.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_inactive.xml new file mode 100644 index 00000000..f3fddbeb --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_inactive.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_loop_active.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_loop_active.xml new file mode 100644 index 00000000..d36b8bc7 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_loop_active.xml @@ -0,0 +1,36 @@ + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_loop_inactive.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_loop_inactive.xml new file mode 100644 index 00000000..26478bb3 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_loop_inactive.xml @@ -0,0 +1,36 @@ + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_play_not_pressed.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_play_not_pressed.xml new file mode 100644 index 00000000..9d95f34f --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_play_not_pressed.xml @@ -0,0 +1,37 @@ + + + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_play_pressed.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_play_pressed.xml new file mode 100644 index 00000000..cb1d3186 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_play_pressed.xml @@ -0,0 +1,37 @@ + + + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_stop_playing_not_pressed.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_stop_playing_not_pressed.xml new file mode 100644 index 00000000..92bef5e8 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_stop_playing_not_pressed.xml @@ -0,0 +1,35 @@ + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_stop_playing_pressed.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_stop_playing_pressed.xml new file mode 100644 index 00000000..c1c38b1e --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_speaker_stop_playing_pressed.xml @@ -0,0 +1,35 @@ + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_spotlight_disabled.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_spotlight_disabled.xml new file mode 100644 index 00000000..f4d66cc0 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_spotlight_disabled.xml @@ -0,0 +1,38 @@ + + + + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_spotlight_enabled.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_spotlight_enabled.xml new file mode 100644 index 00000000..e681c216 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_ic_spotlight_enabled.xml @@ -0,0 +1,38 @@ + + + + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_record_button.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_record_button.xml new file mode 100644 index 00000000..7d20a46e --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_record_button.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_start_play.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_start_play.xml new file mode 100644 index 00000000..e2bddca6 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_start_play.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_stop_play.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_stop_play.xml new file mode 100644 index 00000000..046ff448 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_stop_play.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_widget_broadcast_tab.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_widget_broadcast_tab.xml new file mode 100644 index 00000000..f0b30d5a --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_widget_broadcast_tab.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_widget_local_file_tab.xml b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_widget_local_file_tab.xml new file mode 100644 index 00000000..2549bb5b --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/drawable/uxsdk_selector_speaker_widget_local_file_tab.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-uxsdk-beta-accessory/src/main/res/layout/uxsdk_item_audio_file.xml b/android-uxsdk-beta-accessory/src/main/res/layout/uxsdk_item_audio_file.xml new file mode 100644 index 00000000..d635d39a --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/layout/uxsdk_item_audio_file.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-uxsdk-beta-accessory/src/main/res/layout/uxsdk_widget_mfio_config.xml b/android-uxsdk-beta-accessory/src/main/res/layout/uxsdk_widget_mfio_config.xml new file mode 100644 index 00000000..cd502fd3 --- /dev/null +++ b/android-uxsdk-beta-accessory/src/main/res/layout/uxsdk_widget_mfio_config.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + +