Why event channel disables when an asyncTask will execute? In flutter android - flutter

I have written a native code for flutter that will download file from given url. Everything works fine and file will download and store perfectly. The problem is that when i want to retrieve downloaded percentage(download progress). I used event channel. It will work until I tap download button. After I tap download button nothing will retrieve from event channel until the download is finish. The download is a class that implement asyncTask. Even the onProgressUpdate method in asyncTask is unavailable until the download is complete. looks like the only and only one place that I can see progress and that is inside the doInBackGround method.
I see the percentage by logs. I used event.success(percentage) inside while loop to retrieve percentage but got an error that said:
Methods marked with #UiThread must be executed on the main thread
Then used a Handler with mainloop but nothing happened.
and this is where i'm stuck and don't know what to do.
I appreciate any help.
Here is the codes:
Android Side: (MainActivity)
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "bot/native_services";
private static final String STREAM = "bot/native_services/stream";
private Download download = new Download();
static int percentage = 0;
static boolean dlStart = false;
#Override
public void configureFlutterEngine(#NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
EventChannel.StreamHandler handler = new EventChannel.StreamHandler() {
#Override
public void onListen(Object arguments, EventChannel.EventSink events) {
Log.i("MyTestApp","Registered");
Handler h = new Handler(Looper.getMainLooper());
TimerTask timerTask = new TimerTask() {
#Override
public void run() {
h.post(new Runnable() {
#Override
public void run() {
events.success(percentage);
if (percentage == 100) {
events.endOfStream();
}
}
});
}
};
Timer timer = new Timer();
timer.schedule(timerTask,0,100);
if (percentage == 100) {
timer.cancel();
}
}
#Override
public void onCancel(Object arguments) {
}
};
new EventChannel(flutterEngine.getDartExecutor().getBinaryMessenger(),STREAM).setStreamHandler(handler);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(),CHANNEL).setMethodCallHandler((call, result) -> {
if (call.method.equals("openUrl")) {
openUrl(call.argument("url"), result);
}else if(call.method.equals("checkNetwork")) {
checkNetwork(result);
}else if(call.method.equals("downloadFromUrl")) {
downloadFromUrl(call.argument("url"), result);
}
});
}
public void downloadFromUrl(String url,MethodChannel.Result result) {
download.execute(url);
try {
HashMap<String,String> res = download.get();
Log.i("MyTestApp",res.get("status"));
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Android side: (Download class)
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import javax.net.ssl.HttpsURLConnection;
import io.flutter.plugin.common.EventChannel;
class Download extends AsyncTask<String,Integer, HashMap<String,String>>{
String path = "";
#Override
protected HashMap<String,String> doInBackground(String... strings) {
int count = 0;
try {
URL url = new URL(strings[0]);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();
int length = connection.getContentLength();
Log.i("MyTestApp",String.valueOf(length));
File folder = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath() + "/apk");
if (!folder.exists()) {
boolean res = folder.mkdirs();
}
String appName = "bot.apk";
File apk = new File(folder,appName);
path = apk.getPath();
FileOutputStream fos = new FileOutputStream(apk);
InputStream is = connection.getInputStream();
byte[] buffer = new byte[1024];
long totalReaded = 0l;
while ((count = is.read(buffer)) != -1) {
fos.write(buffer, 0, count);
totalReaded += count;
MainActivity.percentage = (int) (totalReaded * 100) / length;
Log.i("Value",String.valueOf( MainActivity.percentage));
}
fos.flush();
fos.close();
is.close();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
HashMap<String,String> result = new <String,String>HashMap();
result.put("status","OK");
result.put("path",path);
return result;
}
#Override
protected void onPostExecute(HashMap<String,String> s) {
super.onPostExecute(s);
}
}
Flutter side:
class CheckForUpdatesState extends State<CheckForUpdates> {
static const streamChannel =
EventChannel('bot/native_services/stream');
Stream<int> percentage = Stream.empty();
checkUpdates() async {
String url = 'App link';
Map data = await NativeService.downloadFromUrl(url);
}
Stream<int> getPercentage() {
percentage = streamChannel.receiveBroadcastStream().map<int>((event) {
print(event);
return event;
});
return percentage;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Checking for updates'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Looking for latest version'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [Text('Version 1.0.0')],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: checkUpdates,
child: Text(
'Check for updates',
style: TextStyle(color: Colors.green),
))
],
),
StreamBuilder(
initialData: percentage,
stream: getPercentage(),
builder: (context, snapshot) {
if (snapshot.hasData) {
print('snap shot data is: ${snapshot.data}');
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [Text('percentage:${snapshot.data}')],
);
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [Text('percentage:${snapshot.error}')],
);
}
},
)
],
),
);
}
}

Related

flutter : provider is not updating UI and Not Clearing the variables

This is my Query provider
In this provider My data is not updating UI not clearing variable of this function clearRaisedQueryDetailsLoad()
import 'dart:developer';
import 'package:flutter/material.dart';
class QueryProvider with ChangeNotifier {
//raised query details
bool _isRaisedQueryDetailsLoad = false;
bool get isRaisedQueryDetailsLoad => _isRaisedQueryDetailsLoad;
List _raisedQueryDetailsData = [];
List get raisedQueryDetailsData => _raisedQueryDetailsData;
var _raisedQueryDetailsSubject = '';
String get raisedQueryDetailsSubject => _raisedQueryDetailsSubject;
var _raisedQueryDetailsAttachmentUrl = '';
String get raisedQueryDetailsAttachmentUrl =>
_raisedQueryDetailsAttachmentUrl;
var _raisedQueryDetailsStatus;
dynamic get raisedQueryDetailsStatus => _raisedQueryDetailsStatus;
void addIsRaisedQueryDetailsLoad(bool isRaisedQueryDetailsLoad1) async {
_isRaisedQueryDetailsLoad = isRaisedQueryDetailsLoad1;
notifyListeners();
}
void addRaisedQueryDetailsData(dynamic raisedQueryDetailsData1) async {
for (int i = 0; i < raisedQueryDetailsData1["query_details"].length; i++) {
_raisedQueryDetailsData.add(raisedQueryDetailsData1["query_details"][i]);
}
_raisedQueryDetailsSubject = raisedQueryDetailsData1["query_subject"];
_raisedQueryDetailsAttachmentUrl = raisedQueryDetailsData1["attachment_url"];
_raisedQueryDetailsStatus = raisedQueryDetailsData1["query_status"];
print("_raisedQueryDetailsData $_raisedQueryDetailsData");
print("_raisedQueryDetailsAttachmentUrl $_raisedQueryDetailsAttachmentUrl");
print("_raisedQueryDetailsStatus $_raisedQueryDetailsStatus");
print("_raisedQueryDetailsData ${_raisedQueryDetailsData.runtimeType}");
print("_raisedQueryDetailsData ${_raisedQueryDetailsData.length}");
notifyListeners();
}
getRaisedQueryDetailsData() {
return _raisedQueryDetailsData;
}
getRaisedQueryDetailsDataStatus() {
return _raisedQueryDetailsStatus;
}
getRaisedQueryDetailsDataAttachment() {
return _raisedQueryDetailsAttachmentUrl;
}
getIsRaisedQueryDetailsLoad() {
print(_isRaisedQueryDetailsLoad);
return _isRaisedQueryDetailsLoad;
}
getIsRaisedQueryDetailsSubject() {
return _raisedQueryDetailsSubject;
}
void clearRaisedQueryDetailsLoad() {
print("clear");
_raisedQueryDetailsStatus = null;
_raisedQueryDetailsSubject = '';
_raisedQueryDetailsAttachmentUrl = '';
_raisedQueryDetailsData.clear();
notifyListeners();
}
}
queryProvider.clearRaisedQueryDetailsLoad(); // Here When I am calling this api fuction multiple time then my provider data is storing duplicate data(This function is bellow)
and second issue is when I am calling raisedQueryDetailsApiCalled() this function from a dialog box then My UI data is not changing
QueryProvider queryProvider = QueryProvider();
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
queryProvider = Provider.of<QueryProvider>(context, listen: false);
});
// TODO: implement initState
super.initState();
print(widget.queryData["id"]);
raisedQueryDetailsApiCalled();
}
raisedQueryDetailsApiCalled() async {
queryProvider.clearRaisedQueryDetailsLoad(); // Here When I am calling this api fuction multiple time then my provider data is storing duplicate data
if (mounted) {
setState(() {
queryProvider.addIsRaisedQueryDetailsLoad(true);
});
}
apiManager.queryRaisedDetailsApi(context, widget.queryData["id"]).then(
(val) {
if (val["code"] == 200) {
setState(() {
queryProvider.addRaisedQueryDetailsData(val["data"]);
});
}
if (mounted) {
setState(() {
queryProvider.addIsRaisedQueryDetailsLoad(false);
});
}
},
);
}
This is my UI code
queryProvider.getIsRaisedQueryDetailsLoad()
? SingleChildScrollView(
child: Column(
children: [
ProductListSkeleton(),
ProductListSkeleton(),
ProductListSkeleton(),
ProductListSkeleton(),
ProductListSkeleton(),
ProductListSkeleton()
],
),
)
: queryProvider1.getRaisedQueryDetailsData().length == 0
? Container(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [Text("Data not available")],
),
),
)
: Column(
children: [
_bodyChat(queryProvider1),
],
),
Try using the getter methods you have already instead of the general methods and see how that works. And maybe for the widgets code try to make use of consumers.
Consumer Class - provider library

Not able to use Binance_spot Package in Flutter Dart

I am trying for the past two days to Get user data from binance Api with the Binance test net key using package https://pub.dev/packages/binance_spot/example
I tried every way possible I learned as I am a beginner I am not able to get the user data from the user account
I also changed the links that work for the testnet finance API version
here is the example code snippet
import 'dart:async';
import 'package:binance_spot/binance_spot.dart';
import 'package:flutter/material.dart' hide Interval;
class BinanceScreen extends StatefulWidget {
const BinanceScreen({Key? key}) : super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<BinanceScreen> {
BinanceSpot binanceSpot = BinanceSpot(
key: "3HKUUgtwu8WFWh4q31C5bh1veQqMEkEbF07hpMrq8xnwYsWDnj0ZWgYQkvC3gnE0",
secret: "KWwvWOi4nu8s0Qsi87iJ523cfp9Jcl8mFwt2hZHptMyahhGsxnmvdURIxVa9zA74",
);
double lastClosePrice = 0;
String tradablePairs = "";
String lastEventData = "";
WsAccountUpdate? balance;
late StreamSubscription<dynamic> klineStreamSub;
late StreamSubscription<dynamic> userdataStreamSub;
#override
void initState() {
startKlineStream();
startUserdataStream();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Binance API tester"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("Current BTC price : $lastEventData"),
// Text("Last userdataStream event : ${balance}"),
TextButton(
onPressed: getTradablePairs,
child: const Text("GET PAIRS"),
),
Expanded(
flex: 1,
child: SelectableText(
tradablePairs,
maxLines: 200,
minLines: 1,
),
),
],
),
),
);
}
void startKlineStream() {
var stream = binanceSpot.klineStream(
symbol: "BTCUSDT",
interval: Interval.INTERVAL_5m,
);
klineStreamSub = stream.listen(handleNewKline);
}
void handleNewKline(WsKlineEvent event) {
setState(() {
lastClosePrice = event.kline.close;
});
}
void startUserdataStream() async {
var response = await binanceSpot.createListenKey();
if (response.isRight) {
var stream = binanceSpot.userDataStream(listenKey: response.right);
userdataStreamSub = stream.listen(handleUserdataEvent);
} else {
lastEventData = response.left;
}
}
void handleUserdataEvent(dynamic event) {
if (event is WsAccountUpdate) {
lastEventData =
"Account update event : ${event.balances.length} balances updated";
} else if (event is WsBalanceUpdate) {
lastEventData = "Balance update event : ${event.asset} balance updated";
} else if (event is WsExecutionReport) {
lastEventData =
"Execution report event : status is ${event.orderStatus.toStr()}";
} else if (event is WsListOrderStatus) {
lastEventData =
"ListOrder update event : status is ${event.listOrderStatus}";
} else {
lastEventData = "Unknown event type : ${event.toString()}";
}
}
void getTradablePairs() async {
var response = await binanceSpot.exchangeInfo();
if (response.isLeft) {
tradablePairs = response.left;
} else {
var listSymbol = response.right.symbols.map((e) => e.symbol).toList();
tradablePairs = "";
for (var s in listSymbol) {
tradablePairs += "$s ";
}
}
}
#override
void dispose() {
klineStreamSub.cancel();
userdataStreamSub.cancel();
super.dispose();
}
}
and here is the class code snippet which I want to acess
class WsOcoOrder {
String symbol;
int orderId;
String clientOrderId;
WsOcoOrder.fromMap(Map m)
: symbol = m['s'],
orderId = m['i'],
clientOrderId = m['c'];
}
What is the Possible way to access this class and its data in the above-given code snippet
Please refer me to the solution or a Link from where I can learn and implement by myself

How to loop the audio to keep playing until 3 times-Flutter

I am currently using audioplayers: ^0.20.1 to play and resume the video, right now I would like to loop the audio 3 times (keep looping to play the audio). As I know audioplayers package has loop property but I still don't know how to custom the loop property
Here is how you can loop the audio player:
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: AudioPlayerLoopTesting()));
}
class AudioPlayerLoopTesting extends StatefulWidget {
#override
_AudioPlayerLoopTestingState createState() => _AudioPlayerLoopTestingState();
}
class _AudioPlayerLoopTestingState extends State<AudioPlayerLoopTesting> {
AudioCache audioCache = AudioCache();
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
void _playLoopAudio() {
int timesPlayed = 0;
const timestoPlay = 3;
//audio.mp3 is the local asset file
audioCache.play('audio.mp3').then((player) {
player.onPlayerCompletion.listen((event) {
timesPlayed++;
if (timesPlayed >= timestoPlay) {
timesPlayed = 0;
player.stop();
} else {
player.resume();
}
});
});
}
Widget localAsset() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Loop Local Asset'),
ElevatedButton(
child: const Text('Loop the audio'),
onPressed: _playLoopAudio,
),
],
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: localAsset(),
),
);
}
}
Thumbs up if problem resolved
to loop the audio try this :-
static AudioCache musicCache;
static AudioPlayer instance;
void playLoopedMusic() async {
musicCache = AudioCache(prefix: "audio/");
instance = await musicCache.loop("bgmusic.mp3");
// await instance.setVolume(0.5); you can even set the volume
}
void pauseMusic() {
if (instance != null) {
instance.pause(); }}
to loop it 3 time only :-
int numberOftimesToPlay=0;
playThreeTimes(){
_audioPlayer = AudioPlayer();
int res = await _audioPlayer.play("https://192.168.1.66/$sUrl");
//await _audioPlayer.play("http://192.168.1.5/00001.mp3");
if (res == 1 & numberOftimesToPlay>4) {
numberOftimesToPlay ++;
playThreeTimes()
print("ok");
} else {
print("done");
}}

How to set two different lists at the load (initial) of the page by BloC pattern in the flutter

I have two List for DropDownList in my form, I use BloC pattern, I yield 4 State for one Event in demand_bloc.dart page.
#override
Stream<DemandAddState> mapEventToState(DemandAddEvent event) async* {
if (event is InitListLoadEvent) {
yield* _setInitListToState(event);
}
}
Stream<DemandAddState> _setInitListToState(InitListLoadEvent event) async* {
yield InitListLoading();
try {
List data = await demandAddRepository.getEmployeeList();
yield EmployeeListLoaded(data: data);
List data2 = await demandAddRepository.getDayOffList();
yield DayOffListLoaded(data2: data2);
} catch (_) {
yield InitListError();
}
}
my demand_state.dart is
class InitListLoading extends DemandAddState {}
class InitListError extends DemandAddState {}
class EmployeeListLoaded extends DemandAddState {
final List data;
const EmployeeListLoaded({#required this.data}) : assert(data != null);
}
class DayOffListLoaded extends DemandAddState {
final List data2;
const DayOffListLoaded({#required this.data2}) : assert(data2 != null);
}
after, and on my demand_page.dart,
I need to give data, data2,
but unfortunately, data is became Null.
body: BlocProvider<DemandAddBloc>(
create: (context) {
return DemandAddBloc(demandAddRepository: demandAddRepository)
..add(InitListLoadEvent());
},
child: BlocBuilder<DemandAddBloc, DemandAddState>(
builder: (context, demandAddState) {
if (demandAddState is InitListLoading) {
return LoadingIndicator();
}
if (demandAddState is DayOffListLoaded) {
data2 = demandAddState.data2;
}
if (demandAddState is EmployeeListLoaded) {
data = demandAddState.data;
}
if (demandAddState is InitListError){ ... }
return ...
ListView(
children: <Widget>[
Form(
key: _formKey,
child: Column(
children: [
_buildDropDownFormField(data),
_buildDropDownFormField(data2),
....
I lost data after the end process
That's because BLoC only has 1 state at a time. What you can do is either make a separate BLoC to handle both state or make a new state class.
class ListLoaded extends DemandAddState {
final List<Employee> data;
final List<DayOff> data2;
}
body: BlocProvider<DemandAddBloc>(
create: (context) {
return DemandAddBloc(demandAddRepository: demandAddRepository)
..add(InitListLoadEvent());
},
child: BlocBuilder<DemandAddBloc, DemandAddState>(
builder: (context, demandAddState) {
if (demandAddState is InitListLoading) {
return LoadingIndicator();
}
if (demandAddState is ListLoaded) {
data = demandAddState.data;
data2 = demandAddState.data2;
return ...
ListView(
children: <Widget>[
Form(
key: _formKey,
child: Column(
children: [
_buildDropDownFormField(data),
_buildDropDownFormField(data2),
....
}
if (demandAddState is InitListError){
return ..
}
I changed my _state.dart
class InitListLoading extends DemandAddState {}
class InitListError extends DemandAddState {}
class InitListLoaded extends DemandAddState {
final List data;
final List data2;
const InitListLoaded({#required this.data, #required this.data2}) : assert(data != null, data2 != null);
}
and my _page.dart
...
if (demandAddState is InitListLoaded) {
dataDayOffList = demandAddState.data2;
dataEmployeeList = demandAddState.data;
}
...

Detect Mock Location is enabled or disabled in Flutter

My question is that I am using flutter platform to develop an app for my client and I want that my developed app should be able to be detect mock location status from android phone settings so I can check whether the location is coming from gps provider or mock location app. And if mock location is enabled then my app should throw an error msg
i had the same problem and i fixed it by coding in java and implement in flutter project.
here is what i did:
1) add this to your Main_Activity in flutter project.
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import java.util.List;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "samples.flutter.io/location";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
new MethodCallHandler() {
#Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getLocation")) {
boolean b = getMockLocation();
result.success(b);
} else {
result.notImplemented();
}
}
});
}
public static boolean isMockSettingsON(Context context) {
// returns true if mock location enabled, false if not enabled.
if (VERSION.SDK_INT >= VERSION_CODES.CUPCAKE) {
if (Settings.Secure.getString(context.getContentResolver(),
Settings.Secure.ALLOW_MOCK_LOCATION).equals("0"))
return false;
else
return true;
}
return false;
}
public static boolean areThereMockPermissionApps(Context context) {
int count = 0;
PackageManager pm = context.getPackageManager();
List<ApplicationInfo> packages =
pm.getInstalledApplications(PackageManager.GET_META_DATA);
for (ApplicationInfo applicationInfo : packages) {
try {
PackageInfo packageInfo = pm.getPackageInfo(applicationInfo.packageName,
PackageManager.GET_PERMISSIONS);
// Get Permissions
String[] requestedPermissions = packageInfo.requestedPermissions;
if (requestedPermissions != null) {
for (int i = 0; i < requestedPermissions.length; i++) {
if (requestedPermissions[i]
.equals("android.permission.ACCESS_MOCK_LOCATION")
&& !applicationInfo.packageName.equals(context.getPackageName())) {
count++;
}
}
}
} catch (PackageManager.NameNotFoundException e) {
Log.e("Got exception " , e.getMessage());
}
}
if (count > 0)
return true;
return false;
}
private boolean getMockLocation() {
boolean b ;
b= areThereMockPermissionApps(MainActivity.this);
return b;
}
}
2) use it in your flutter_dart Code like this:
static const platform = const MethodChannel('samples.flutter.io/location');
bool mocklocation = false;
Future<void> _getMockLocation() async {
bool b;
try {
final bool result = await platform.invokeMethod('getLocation');
b = result;
} on PlatformException catch (e) {
b = false;
}
mocklocation = b;
}
if (mocklocation == true) {
return showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return WillPopScope(
onWillPop: (){},
child: AlertDialog(
title: Text('Location'),
content: Text('Your Location is fake'),
),
);
});
}
3) for more information and example:
https://flutter.dev/docs/development/platform-integration/platform-channels
Barzan's answer is very good, there's also a Flutter package named trust_location, you can find it here.
You can use it as following to check mock location:
bool isMockLocation = await TrustLocation.isMockLocation;
So, I recommend to use it.