I've developed and android chatting app, through the gRPC protocol, problem is, after I send the first message to the server, the server propagates the message to the available clients but then it runs in to this Exception: "io.grpc.StatusRuntimeException: CANCELLED: cancelled before receiving half close"
I've scoured the web for a hint at what could be wrong or a fix but I've got pretty much nothing useful.
This is my server class function that deals with the clients messages
public StreamObserver<Messaging.Message> sendReceiveMessage(StreamObserver<Messaging.Message> responseObserver) {
// return super.sendReceiveMessage(responseObserver);
//While you are on the inside of the chat the bilateral stream will be a continue one
observers.add(responseObserver);
return new StreamObserver<Messaging.Message>() {
#Override
public void onNext(Messaging.Message message) {
//receiving data from client
System.out.println(String.format("Got a message from: '%s' : '%s'", message.getMessageOwnerId(), message.getMessage()));
observers.stream().forEach(o -> {
o.onNext(message);
});
}
#Override
public void onError(Throwable throwable) {
observers.remove(responseObserver);
}
#Override
public void onCompleted() {
observers.remove(responseObserver);
}
};
}
This is the protobuff for the messages
service MessagingService{
rpc GetMessagingHistory(MessageHistoryRequest) returns (stream Message);
rpc SendReceiveMessage(stream Message) returns (stream Message);
}
message MessageHistoryRequest{
int32 chat_id = 1;
int32 last_message_id = 2;
}
message Message{
int32 message_id = 1;
int32 chat_id = 2;
int32 message_owner_id = 3;
string message = 4;
}
and lastly the client side code:
private int CHAT_ID;
private ImageView profilePicture;
private TextView name;
private Button sendButton;
private EditText messageEditText;
private RecyclerView chat;
private MessageAdapter msgAdapter;
private Vector<Messaging.Message> messages = new Vector<>();
private ManagedChannel channel;
private MessagingServiceGrpc.MessagingServiceStub stub;
private StreamObserver<Messaging.Message> toServer;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat);
initViews();
}
private void createStub(){
channel = ManagedChannelBuilder.forAddress("10.0.2.2",8080)
.usePlaintext()
.build();
stub = MessagingServiceGrpc.newStub(channel);
toServer = stub.sendReceiveMessage(new StreamObserver<Messaging.Message>() {
#Override
public void onNext(Messaging.Message value) {
msgAdapter.receiveMessage(value);
Log.e("MYERROR",value.getMessageOwnerId() + " : " + value.getMessage() );
msgAdapter.notifyDataSetChanged();
}
#Override
public void onError(Throwable t) {
//nothing
t.printStackTrace();
}
#Override
public void onCompleted() {
Log.e("Completed:","Why");
}
});
}
private void initViews(){
profilePicture = findViewById(R.id.profilePictureChatMenu);
name = findViewById(R.id.chatNameTextView);
Intent intent = getIntent();
CHAT_ID = intent.getIntExtra("ID",-1);
name.setText(intent.getStringExtra("NAME"));
sendButton = findViewById(R.id.sendCommentButton);
messageEditText = findViewById(R.id.writingBarEditText);
chat = findViewById(R.id.chat);
chat.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
msgAdapter = new MessageAdapter(getApplicationContext(),messages);
chat.setAdapter(msgAdapter);
createStub();
sendButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Messaging.Message sentMessage = Messaging.Message.newBuilder()
.setChatId(CHAT_ID)
.setMessage(messageEditText.getText().toString())
.setMessageOwnerId(ApplicationController.getAccount().getId())
.build();
toServer.onNext(sentMessage);
messageEditText.setText("");
msgAdapter.receiveMessage(sentMessage);
msgAdapter.notifyDataSetChanged();
}
});
}
The message is just informing the server the client cancelled. There's nothing more for the server to do other than clean up resources.
The cancellation may have been triggered because of an I/O failure. The TCP connection may have been dead and sending a message on the connection exposed the fact the TCP connection died (TCP can only reliably detect breakages by performing a write).
Related
I'm using agora to make voice calls in my android application , i have set up the code as per the documentation but whenever i try to join a call it crashes and says that rtcEngine is null even though it is initialized , any help would be appreciated , Thank you
Code
public class AudioCallActivity extends AppCompatActivity {
// An integer that identifies the local user.
private int uid = 0;
// Track the status of your connection
private boolean isJoined = false;
// Agora engine instance
private RtcEngine agoraEngine;
// UI elements
private TextView infoText;
private Button joinLeaveButton;
private static final int PERMISSION_REQ_ID = 22;
private static final String[] REQUESTED_PERMISSIONS = { Manifest.permission.RECORD_AUDIO};
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_call);
// If all the permissions are granted, initialize the RtcEngine object and join a channel.
if (!checkSelfPermission()) {
ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, PERMISSION_REQ_ID);
}
setupVoiceSDKEngine();
// Set up access to the UI elements
joinLeaveButton = findViewById(R.id.joinLeaveButton);
infoText = findViewById(R.id.infoText);
}
private boolean checkSelfPermission() {
return ContextCompat.checkSelfPermission(this, REQUESTED_PERMISSIONS[0]) == PackageManager.PERMISSION_GRANTED;
}
void showMessage(String message) {
runOnUiThread(() -> Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show());
}
private void setupVoiceSDKEngine() {
try {
RtcEngineConfig config = new RtcEngineConfig();
config.mContext = getBaseContext();
config.mAppId = getString(R.string.app_id);
config.mEventHandler = mRtcEventHandler;
agoraEngine = RtcEngine.create(config);
} catch (Exception e) {
throw new RuntimeException("Check the error.");
}
}
private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
#Override
// Listen for the remote user joining the channel.
public void onUserJoined(int uid, int elapsed) {
runOnUiThread(()->infoText.setText("Remote user joined: " + uid));
}
#Override
public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
// Successfully joined a channel
isJoined = true;
showMessage("Joined Channel " + channel);
runOnUiThread(()->infoText.setText("Waiting for a remote user to join"));
}
#Override
public void onUserOffline(int uid, int reason) {
// Listen for remote users leaving the channel
showMessage("Remote user offline " + uid + " " + reason);
if (isJoined) runOnUiThread(()->infoText.setText("Waiting for a remote user to join"));
}
#Override
public void onLeaveChannel(RtcStats stats) {
// Listen for the local user leaving the channel
runOnUiThread(()->infoText.setText("Press the button to join a channel"));
isJoined = false;
}
};
private void joinChannel() {
ChannelMediaOptions options = new ChannelMediaOptions();
options.autoSubscribeAudio = true;
// Set both clients as the BROADCASTER.
options.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER;
// Set the channel profile as BROADCASTING.
options.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
// Join the channel with a temp token.
// You need to specify the user ID yourself, and ensure that it is unique in the channel.
agoraEngine.joinChannel(getString(R.string.agora_token), "ChannelOne", uid, options);
}
public void joinLeaveChannel(View view) {
try {
if (isJoined) {
agoraEngine.leaveChannel();
joinLeaveButton.setText("Join");
} else {
joinChannel();
joinLeaveButton.setText("Leave");
}
}catch (Exception e){
Log.d("TAG","Error is " + e.getMessage());
}
}
#Override
protected void onDestroy() {
agoraEngine.leaveChannel();
// Destroy the engine in a sub-thread to avoid congestion
new Thread(() -> {
RtcEngine.destroy();
agoraEngine = null;
}).start();
onDestroy();
}
}
Error Thrown
Attempt to invoke virtual method 'int io.agora.rtc2.RtcEngine.joinChannel(java.lang.String, java.lang.String, int, io.agora.rtc2.ChannelMediaOptions)' on a null object reference
I'm working on app that will use biometric as an option to login. Before I use the actual biometric prompt I need to check one thing from server - I use AsyncTask to do it. So, to sum up - I invoke AsyncTask from Parent Activity (login.java), and then AsyncTask uses biometricUtils.java class, that makes biometric prompt. The point is, I keep passing null instead of context to biometricUtils.java:
Attempt to invoke virtual method 'java.util.concurrent.Executor android.content.Context.getMainExecutor()' on a null object reference at biometricUtils.<init>(biometricUtils.java:34)
I have no idea to pass the context correctly.
Here's my code:
login.java
public class login extends AppCompatActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.login);
Bundle bundle = getIntent().getExtras();
final boolean flag = false;
final String androidID = bundle.getString("androidID");
final Activity thisActivity = this;
final Context context = getApplicationContext();
// login using biometrics
Button btnBiometricLogin = findViewById(R.id.btnBiometricLogin);
btnBiometricLogin.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
checkAndroidID async = new checkAndroidID(context);
async.getParentActivity(thisActivity);
async.setFlag(flag);
async.execute(androidID);
}
});
}
}
checkAndroidID.java
public class checkAndroidID extends AsyncTask <String, Void, String> {
openHTTP openHTTP = new openHTTP();
requestHTTP requests = new requestHTTP();
Activity parentActivity;
private WeakReference<Context> contextRef;
Boolean flag;
public checkAndroidID(Context context){
contextRef = new WeakReference<>(context);
}
public void getParentActivity(Activity parentActivity){
this.parentActivity = parentActivity;
}
public void setFlag (Boolean flag){
this.flag = flag;
}
#Override
protected String doInBackground(String... strings) {
try {
HttpURLConnection httpConn = openHTTP.prepareConnection("url");
String json = "{ \"androidID\": \"" + strings[0] + "\" }";
requests.sendData(json, httpConn);
return requests.receiveData(httpConn);
} catch (Exception e){
e.printStackTrace();
}
return null;
}
#Override
protected void onPostExecute(String s) {
String[] result = s.split(";");
Context ctx = contextRef.get();
if (result[0].equals("TRUE")) flag = true;
if (!flag) Toast.makeText(parentActivity, "Biometric authentication is now unavailable." +
" Please login using username and password", Toast.LENGTH_SHORT).show();
else {
biometricUtils biometrics = new biometricUtils(ctx);
biometrics.getParentActivity(parentActivity);
biometrics.getUsername(result[1]);
biometrics.inovkeBiometricPrompt();
}
super.onPostExecute(s);
}
}
and biometricUtlis.java
public class biometricUtils {
Activity parentActivity;
String username;
Context context;
public void getParentActivity(Activity parentActivity){
this.parentActivity = parentActivity;
}
public void getUsername(String s){
this.username = s;
}
public biometricUtils(Context context){
this.context = context;
}
// creating a variable for our Executor
Executor executor = ContextCompat.getMainExecutor(context); // LINE 34
// this will give us result of AUTHENTICATION
final BiometricPrompt biometricPrompt = new BiometricPrompt((FragmentActivity) parentActivity, executor, new BiometricPrompt.AuthenticationCallback() {
#Override
public void onAuthenticationError(int errorCode, #NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
}
// THIS METHOD IS CALLED WHEN AUTHENTICATION IS SUCCESS
#Override
public void onAuthenticationSucceeded(#NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
Intent intent = new Intent(parentActivity.getApplicationContext(), tmp.class);
intent.putExtra("username", username);
parentActivity.startActivity(intent);
}
#Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
});
// creating a variable for our promptInfo
// BIOMETRIC DIALOG
final BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder().setTitle("Biometrical login")
.setDescription("Place your fingerprint on scanner to proceed").setNegativeButtonText("Cancel").build();
public void inovkeBiometricPrompt() {
biometricPrompt.authenticate(promptInfo);
}
}
I am trying to implement a ViewModel architecture for a RecyclerView in AndroidX, following the example as stated in enter link description here and enter link description here. Items in the recyclerView get selected on position clicked, but for some reason, the selected item de-select and revert to default after the device is rotated and configuration changed. I know there have been answers for questions like this in the past, but all I have seen are either not directly applicable in my case or are simply for deprecated cases.
CAN SOMEONE PLEASE TELL ME WHAT I AM DOING WRONG!
Below are snippets from my Code:
Dependencies added
dependencies {
def lifecycle_version = "2.2.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
// Saved state module for ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
// Annotation processor
annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
}
Repository Class:
public class TopicRepository {
private Application application;
private SharedPreferences sharedPreferences;
private ArrayList<RootTopic> topicGroupList;
private MutableLiveData<ArrayList<RootTopic>>topicGroupMLD;
public TopicRepository(Application application) {
this.application = application;
}
public LiveData<ArrayList<RootTopic>> getRootTopicLD(String subject){
if (topicGroupMLD == null){
topicGroupMLD = new MutableLiveData<ArrayList<RootTopic>>();
generateTopicGroup(subject);
}
return topicGroupMLD;
}
private void generateTopicGroup(final String subject){
Log.d(TAG, "generateTopicGroup: CALLED");
isRequestingMLD.postValue(true);
final String subjectTopicGroupList = subject + "TopicGroupList";
sharedPreferences = application.getSharedPreferences(AppConstant.Constants.PACKAGE_NAME, Context.MODE_PRIVATE);
String serializedTopicGroup = sharedPreferences.getString(subjectTopicGroupList, null);
if (serializedTopicGroup != null){
Gson gson = new Gson();
Type type = new TypeToken<ArrayList<RootTopic>>(){}.getType();
topicGroupList = gson.fromJson(serializedTopicGroup, type);
topicGroupMLD.postValue(topicGroupList);
}else {// - Not saved in SP
Log.d(TAG, "getTopicGroup: NOT IN SP");
new ActiveConnectionCheck(new ActiveConnectionCheck.Consumer() {
#Override
public void accept(Boolean internet) {
Log.d(TAG, "accept: CHECKED INTERNET");
if (internet){
Log.d(TAG, "accept: INTERNET CONNECTION = TRUE");
internetCheckMLD.postValue(AppConstant.Constants.IS_INTERNET_REQUEST_SUCCESS);
FirebaseFirestore fbFStore = FirebaseFirestore.getInstance();
CollectionReference lectureRef = fbFStore.collection(subject);
lectureRef.orderBy(AppConstant.Constants.POSITION, Query.Direction.ASCENDING)
.get().addOnSuccessListener(
new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
ArrayList<Topic>topicList = new ArrayList<>();
ArrayList<String> rootTitleList = new ArrayList<>();
for (QueryDocumentSnapshot snapshot : queryDocumentSnapshots){
Topic topic = snapshot.toObject(Topic.class);
topicList.add(topic);
}
Log.d(TAG, "onSuccess: TopicListSize = " + topicList.size());
for (Topic topic : topicList){
String rootTopicString = topic.getRootTopic();
if (!rootTitleList.contains(rootTopicString)){
rootTitleList.add(rootTopicString);
}
}
Log.d(TAG, "onSuccess: RootTitleListSize = " + rootTitleList.size());
for (int x = 0; x < rootTitleList.size(); x ++){
RootTopic rootTopic = new RootTopic(rootTitleList.get(x), new ArrayList<Topic>());
topicGroupList = new ArrayList<>();
topicGroupList.add(rootTopic);
}
for (int e = 0; e < topicList.size(); e++){
addTopicToGroup(topicGroupList, topicList.get(e));
}
topicGroupMLD.postValue(topicGroupList);
Gson gson = new Gson();
String serializedTopicGroup = gson.toJson(topicGroupList);
sharedPreferences.edit().putString(subjectTopicGroupList, serializedTopicGroup).apply();
Log.d(TAG, "onSuccess: TOPICGROUPSIZE = " + topicGroupList.size());
Log.d(TAG, "onSuccess: SERIALIZED GROUP = " + serializedTopicGroup);
isRequestingMLD.postValue(false);
}
}
).addOnFailureListener(
new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
isRequestingMLD.postValue(false);
Log.d(TAG, "onFailure: FAILED TO GET TOPICLIST e = " + e.toString());
}
}
);
}else {
internetCheckMLD.postValue(AppConstant.Constants.IS_INTERNET_REQUEST_FAIL);
Log.d(TAG, "accept: InternetCONECTION = " + false);
}
}
});
}
}
private void addTopicToGroup(ArrayList<RootTopic>rootGroup, Topic topic){
for (int x = 0; x < rootGroup.size(); x++){
RootTopic rootTopic = rootGroup.get(x);
if (rootTopic.getRootTopicName().equals(topic.getRootTopic())){
rootTopic.getTopicGroup().add(topic);
}
}
}
}
My ViewModel class
public class LectureViewModel extends AndroidViewModel {
public static final String TAG = AppConstant.Constants.GEN_TAG + ":LectureVM";
private Application application;
private TopicRepository topicRepository;
private ArrayList<RootTopic> topicGroupList;
public LectureViewModel(#NonNull Application application) {
super(application);
this.application = application;
topicRepository = new TopicRepository(application);
}
public LiveData<ArrayList<RootTopic>> getRootTopicListLD(String subject){
return topicRepository.getRootTopicLD(subject);
}
}
Activity Implementing ViewModel
public class LectureRoomActivity extends AppCompatActivity {
public static final String TAG = AppConstant.Constants.GEN_TAG + " LecRoom";
private LectureViewModel lectureRoomVM;
private String subject;
private RecyclerView mainRecyclerView;
private RootTopicAdapter rootTopicAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_lecture_room);
Intent intent = getIntent();
subject = intent.getStringExtra(AppConstant.Constants.SUBJECT);
mainRecyclerView = findViewById(R.id.recyclerView);
downloadVM = new ViewModelProvider(this).get(DownloadLectureViewModel.class);
lectureRoomVM = new ViewModelProvider(this).get(LectureViewModel.class);
lectureRoomVM.getRootTopicListLD(subject).observe(
this,
new Observer<ArrayList<RootTopic>>() {
#Override
public void onChanged(ArrayList<RootTopic> rootTopics) {
if (rootTopics != null){
currentTopic = lectureRoomVM.getCursorTopic(subject, rootTopics);
setUpRec(rootTopics, currentTopic);
}
}
});
}
private void setUpRec( ArrayList<RootTopic>topicGroup, CursorTopic currentTopic){
rootTopicAdapter = new RootTopicAdapter(topicGroup,
new ArrayList<String>(), currentTopic.getParentPosition(),
currentTopic.getCursorPosition());
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(
this, RecyclerView.VERTICAL,false);
mainRecyclerView.setHasFixedSize(true);
mainRecyclerView.setLayoutManager(linearLayoutManager);
mainRecyclerView.setAdapter(rootTopicAdapter);
Log.d(TAG, "setUpRec: SETTING REC");
}
}
For saving and restoring UI related data you better use savedInstanceState Bundle to survive the last state. To achive this you simply override two methods in you UI activity. See the sample code snippet below.
In your RootTopicAdapter
// Add this where you detect the item click, probably in your adaptor class
private int lastRecyclerViewIndex; // define the variable to hold the last index
...
#Override
public void onClick(View v) {
lastRecyclerViewIndex = getLayoutPosition();
}
public int getLastIndex() {
return lastRecyclerViewIndex;
}
In your view model class
public class LectureViewModel extends AndroidViewModel {
public static final String TAG = AppConstant.Constants.GEN_TAG + ":LectureVM";
private Application application;
private TopicRepository topicRepository;
private ArrayList<RootTopic> topicGroupList;
public boolean mustRestore; // Is there any data to restore
public int lasIndexSelected;
public LectureViewModel(#NonNull Application application) {
super(application);
this.application = application;
topicRepository = new TopicRepository(application);
}
public LiveData<ArrayList<RootTopic>> getRootTopicListLD(String subject){
return topicRepository.getRootTopicLD(subject);
}
}
In you UI activity which uses the RecyclerView
public class LectureRoomActivity extends AppCompatActivity {
...
private LectureViewModel lectureRoomVM;
...
private RecyclerView mainRecyclerView;
private RootTopicAdapter rootTopicAdapter;
...
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_lecture_room);
Intent intent = getIntent();
subject = intent.getStringExtra(AppConstant.Constants.SUBJECT);
mainRecyclerView = findViewById(R.id.recyclerView);
downloadVM = new ViewModelProvider(this).get(DownloadLectureViewModel.class);
lectureRoomVM = new ViewModelProvider(this).get(LectureViewModel.class);
lectureRoomVM.getRootTopicListLD(subject).observe(
this,
new Observer<ArrayList<RootTopic>>() {
#Override
public void onChanged(ArrayList<RootTopic> rootTopics) {
if (rootTopics != null){
currentTopic = lectureRoomVM.getCursorTopic(subject, rootTopics);
setUpRec(rootTopics, currentTopic);
// Exactly here, after setting up the data get your index for example
if(lectureRoomVM.mustRestore){
// Check the item count in the adaptor to avoid crashes
if(mainRecyclerView.getAdapter().getItemCount >= lastRecyclerViewIndex){
mainRecyclerView.findViewHolderForAdapterPosition(lastRecyclerViewIndex).itemView.performClick();
}
// After the restoration set the mustRestore to false
lectureRoomVM.mustRestore = false;
}
}
}
});
}
#Override
protected void onDestroy() {
super.onDestroy();
Log.d(E, "onDestroy");
/*
* Here just set the mustRestore to true in order to be able to restore in onCreate method.
* If the application itself is not destroyed your data will still be live in the
* memory thanks to the ViewModel's life cycle awarness.
*/
lectureRoomVM.mustRestore = true;
}
}
There you go. Try this logic carefully without bugs. Then I think you will achive what you want to get.
I am trying to write some custom data to a BLE device with a custom service on it. I have followed this tutorial: https://www.youtube.com/watch?v=vUbFB1Qypg8&feature=emb_logo which is from android.
I can see my custom service with data on it. Also via another application (NRF connect) I can write data to this custom service. Therefore, I know its possible to write data to the service.
The issue I have is other examples of how to write data to the custom service use BluetoothGatt whereas, this code seems to use BluetoothLeService instead. This is an issue as the documentation does not seem to have a read API for the BluetoothLeService. So I can not use something like mBluetoothGatt.readCharacteristic(mReadCharacteristic);
How can I go about creating a function to write a value (integer) to the characterisitic.
My UUID for the service is: "f3641400-00b0-4240-ba50-05ca45bf8abc"
My UUID for the characteristic is: "f3641401-00b0-4240-ba50-05ca45bf8abc"
For reference my code is below:
public class DeviceControlActivity extends Activity {
private final static String TAG =
DeviceControlActivity.class.getSimpleName();
public static final String EXTRAS_DEVICE_NAME = "DEVICE_NAME";
public static final String EXTRAS_DEVICE_ADDRESS = "DEVICE_ADDRESS";
private TextView mConnectionState;
private TextView mDataField;
private EditText Command_string;
private BluetoothGatt mGatt;
private Button btn_gatt_connect;
private Button btn_live,btn_command;
private String mDeviceName;
private String mDeviceAddress;
private String User_data;
private ExpandableListView mGattServicesList;
private BluetoothLeService mBluetoothLeService;
private ArrayList<ArrayList<BluetoothGattCharacteristic>> mGattCharacteristics =
new ArrayList<ArrayList<BluetoothGattCharacteristic>>();
private boolean mConnected = false;
private BluetoothGattCharacteristic mNotifyCharacteristic;
private final String LIST_NAME = "NAME";
private final String LIST_UUID = "UUID";
private String Switch_case ="Connect";
private boolean Write_ble_command = true;
// Code to manage Service lifecycle.
private final ServiceConnection mServiceConnection = new ServiceConnection() {
#Override
public void onServiceConnected(ComponentName componentName, IBinder service) {
mBluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService();
if (!mBluetoothLeService.initialize()) {
Log.e(TAG, "Unable to initialize Bluetooth");
finish();
}
// Automatically connects to the device upon successful start-up initialization.
mBluetoothLeService.connect(mDeviceAddress);
}
#Override
public void onServiceDisconnected(ComponentName componentName) {
mBluetoothLeService = null;
}
};
// Handles various events fired by the Service.
// ACTION_GATT_CONNECTED: connected to a GATT server.
// ACTION_GATT_DISCONNECTED: disconnected from a GATT server.
// ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services.
// ACTION_DATA_AVAILABLE: received data from the device. This can be a result of read
// or notification operations.
private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
mConnected = true;
updateConnectionState(R.string.connected);
invalidateOptionsMenu();
} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
mConnected = false;
updateConnectionState(R.string.disconnected);
invalidateOptionsMenu();
clearUI();
} else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
// Show all the supported services and characteristics on the user interface.
displayGattServices(mBluetoothLeService.getSupportedGattServices());
} else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
}
}
};
// If a given GATT characteristic is selected, check for supported features. This sample
// demonstrates 'Read' and 'Notify' features. See
// http://d.android.com/reference/android/bluetooth/BluetoothGatt.html for the complete
// list of supported characteristic features.
private final ExpandableListView.OnChildClickListener servicesListClickListner =
new ExpandableListView.OnChildClickListener() {
#Override
public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
int childPosition, long id) {
if (mGattCharacteristics != null) {
final BluetoothGattCharacteristic characteristic =
mGattCharacteristics.get(groupPosition).get(childPosition);
final int charaProp = characteristic.getProperties();
if ((charaProp | BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
// If there is an active notification on a characteristic, clear
// it first so it doesn't update the data field on the user interface.
if (mNotifyCharacteristic != null) {
mBluetoothLeService.setCharacteristicNotification(
mNotifyCharacteristic, false);
mNotifyCharacteristic = null;
}
mBluetoothLeService.readCharacteristic(characteristic);
}
if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
mNotifyCharacteristic = characteristic;
mBluetoothLeService.setCharacteristicNotification(
characteristic, true);
}
return true;
}
return false;
}
};
private void Gatt_Connect()
{
switch(Switch_case) {
case "Connect":
btn_gatt_connect.setText("Disconnect");//Presently connected allow to disconnect
mBluetoothLeService.connect(mDeviceAddress);
Switch_case = "Disconnect";
break;
case "Disconnect":
btn_gatt_connect.setText("Connect");//Presently disconnected allow to connect
mBluetoothLeService.disconnect();
Switch_case = "Connect";
break;
}
}
private void Live_data(){
//Ensure you are disconnected
btn_gatt_connect.setText("Connect");//Presently disconnected allow to connect
mBluetoothLeService.disconnect();//Might only run if connected
Switch_case = "Connect";
Intent myIntent = new Intent(DeviceControlActivity.this, MainActivity.class);
Bundle bundle = new Bundle();
bundle.putString("User_data", User_data);//Pass through the user's email address to the main activity for display
myIntent.putExtras(bundle);
DeviceControlActivity.this.startActivity(myIntent);//Run the main activity}
}
private void clearUI() {
mGattServicesList.setAdapter((SimpleExpandableListAdapter) null);
mDataField.setText(R.string.no_data);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.gatt_services_characteristics);
//Get the extras passed in
Bundle bundle = getIntent().getExtras();
mDeviceName = bundle.getString("Device_Name");
mDeviceAddress = bundle.getString("Device_Address");
User_data = bundle.getString("User_data");
// Sets up UI references.
((TextView) findViewById(R.id.device_address)).setText(mDeviceAddress);
Command_string = (EditText)findViewById(R.id.Command_string);
btn_gatt_connect = (Button)findViewById(R.id.btn_gatt_connect);
btn_live = (Button) findViewById(R.id.btn_live);
btn_command = (Button) findViewById(R.id.btn_command);
btn_gatt_connect.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Gatt_Connect();
}
});
btn_gatt_connect.setText("Disconnect");//Presently disconnected allow to connect
btn_live.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Live_data();
}
});
btn_command.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Send_Command();//Sends the command in the text box to the nordic board
}
});
mGattServicesList = (ExpandableListView) findViewById(R.id.gatt_services_list);
mGattServicesList.setOnChildClickListener(servicesListClickListner);
mConnectionState = (TextView) findViewById(R.id.connection_state);
mDataField = (TextView) findViewById(R.id.data_value);
//setTitle(mDeviceAddress);//Change the title to Scanner as the device is no longer being read
Intent gattServiceIntent = new Intent(DeviceControlActivity.this, BluetoothLeService.class);
bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);
}
#Override
protected void onResume() {
super.onResume();
registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter());
if (mBluetoothLeService != null) {
final boolean result = mBluetoothLeService.connect(mDeviceAddress);
Log.d(TAG, "Connect request result=" + result);
}
}
#Override
protected void onPause() {
super.onPause();
unregisterReceiver(mGattUpdateReceiver);
}
#Override
protected void onDestroy() {
super.onDestroy();
unbindService(mServiceConnection);
mBluetoothLeService = null;
}
protected void Send_Command()
{
Write_ble_command = true;//Write command issued on display gatt service
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.gatt_services, menu);
if (mConnected) {
menu.findItem(R.id.menu_connect).setVisible(false);
menu.findItem(R.id.menu_disconnect).setVisible(true);
} else {
menu.findItem(R.id.menu_connect).setVisible(true);
menu.findItem(R.id.menu_disconnect).setVisible(false);
}
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case R.id.menu_connect:
mBluetoothLeService.connect(mDeviceAddress);
return true;
case R.id.menu_disconnect:
mBluetoothLeService.disconnect();
return true;
case android.R.id.home:
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
private void updateConnectionState(final int resourceId) {
runOnUiThread(new Runnable() {
#Override
public void run() {
mConnectionState.setText(resourceId);
}
});
}
private void displayData(String data) {
if (data != null) {
mDataField.setText(data);
}
}
// Demonstrates how to iterate through the supported GATT Services/Characteristics.
// In this sample, we populate the data structure that is bound to the ExpandableListView
// on the UI.
private void displayGattServices(List<BluetoothGattService> gattServices) {
if (gattServices == null) return;
String uuid = null;
String unknownServiceString = getResources().getString(R.string.unknown_service);
String unknownCharaString = getResources().getString(R.string.unknown_characteristic);
ArrayList<HashMap<String, String>> gattServiceData = new ArrayList<HashMap<String, String>>();
ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData
= new ArrayList<ArrayList<HashMap<String, String>>>();
mGattCharacteristics = new ArrayList<ArrayList<BluetoothGattCharacteristic>>();
// Loops through available GATT Services.
for (BluetoothGattService gattService : gattServices) {
HashMap<String, String> currentServiceData = new HashMap<String, String>();
uuid = gattService.getUuid().toString();
currentServiceData.put(
LIST_NAME, SampleGattAttributes.lookup(uuid, unknownServiceString));
currentServiceData.put(LIST_UUID, uuid);
gattServiceData.add(currentServiceData);
ArrayList<HashMap<String, String>> gattCharacteristicGroupData =
new ArrayList<HashMap<String, String>>();
List<BluetoothGattCharacteristic> gattCharacteristics =
gattService.getCharacteristics();
ArrayList<BluetoothGattCharacteristic> charas =
new ArrayList<BluetoothGattCharacteristic>();
// Loops through available Characteristics.
for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
charas.add(gattCharacteristic);
HashMap<String, String> currentCharaData = new HashMap<String, String>();
uuid = gattCharacteristic.getUuid().toString();
currentCharaData.put(
LIST_NAME, SampleGattAttributes.lookup(uuid, unknownCharaString));
currentCharaData.put(LIST_UUID, uuid);
gattCharacteristicGroupData.add(currentCharaData);
}
mGattCharacteristics.add(charas);
gattCharacteristicData.add(gattCharacteristicGroupData);
}
SimpleExpandableListAdapter gattServiceAdapter = new SimpleExpandableListAdapter(
this,
gattServiceData,
android.R.layout.simple_expandable_list_item_2,
new String[] {LIST_NAME, LIST_UUID},
new int[] { android.R.id.text1, android.R.id.text2 },
gattCharacteristicData,
android.R.layout.simple_expandable_list_item_2,
new String[] {LIST_NAME, LIST_UUID},
new int[] { android.R.id.text1, android.R.id.text2 }
);
mGattServicesList.setAdapter(gattServiceAdapter);
}
private static IntentFilter makeGattUpdateIntentFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED);
intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED);
intentFilter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED);
intentFilter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE);
return intentFilter;
}
}
I fixed my issue by creating a function inside of the Bluetoothle service. This is where the bluetooth gatt connection is. The function is like so.
public void writeCharacteristic(int Data) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter not initialized");
return;
}
byte[] value = intToByteArray(Data);
BluetoothGattService mCustomService = mBluetoothGatt.getService(UUID.fromString("f3641400-00b0-4240-ba50-05ca45bf8abc"));
if(mCustomService == null){
Log.w(TAG, "Custom BLE Service not found");
return;
}
/*get the read characteristic from the service*/
BluetoothGattCharacteristic characteristic = mCustomService.getCharacteristic(UUID.fromString("f3641401-00b0-4240-ba50-05ca45bf8abc"));
characteristic.setValue(value);
mBluetoothGatt.writeCharacteristic(characteristic);
}
It just needs a data (Int value passing into it). Then this is received by my Bluetooth board and, can see its data on a terminal being sent correctly.
I'm in the process of implementing in app billing for Android and have got to the point where I can retrieve a list of products from the store. And can activate the Google purchase dialog via calling the launchBillingFlow() method. The documentation indicates that once this has been called, the onPurchasesUpdated is then called with the result. However this isn't happening for me.
The logging confirms that the purchase is requested (from within my method: startPurchaseFlow()). My onPurchasesUpdated() is also called when the activity first runs and provides a OK result (0) to confirm connection set up.
But why isn't it being called after launchBillingFlow()?
Class that holds purchase mechanics:
public class BillingManager implements PurchasesUpdatedListener {
private final BillingClient mBillingClient; // Billing client used to interface with Google Play
private final Store mActivity; // Referenced in constructor
// Structure to hold the details of SKUs returned from querying store
private static final HashMap<String, List<String>> SKUS;
static
{
SKUS = new HashMap<>();
SKUS.put(BillingClient.SkuType.INAPP, Arrays.asList("com.identifier.unlock")); // Strings for in app permanent products
}
public List<String> getSkus(#BillingClient.SkuType String type) {
return SKUS.get(type);
}
// Constructor
public BillingManager(Store activity) {
mActivity = activity;
mBillingClient = BillingClient.newBuilder(mActivity).setListener(this).build(); // Initialise billing client and set listener
mBillingClient.startConnection(new BillingClientStateListener() { // Start connection via billing client
#Override
public void onBillingSetupFinished(#BillingClient.BillingResponse int billingResponse) { // Actions to complete when connection is set up
if (billingResponse == BillingClient.BillingResponse.OK) {
Log.i("dev", "onBillingSetupFinished() response: " + billingResponse);
mActivity.getProducts();
} else {
Log.w("dev", "onBillingSetupFinished() error code: " + billingResponse);
}
}
#Override
public void onBillingServiceDisconnected() { // Called when the connection is disconnected
Log.w("dev", "onBillingServiceDisconnected()");
}
});
}
// Receives callbacks on updates regarding future purchases
#Override
public void onPurchasesUpdated(#BillingClient.BillingResponse int responseCode,
List<Purchase> purchases) {
Log.d(TAG, "onPurchasesUpdated() response: " + responseCode);
if (responseCode == 0 && !purchases.isEmpty()) {
String purchaseToken;
for (Purchase element : purchases) {
purchaseToken = element.getPurchaseToken();
mBillingClient.consumeAsync(purchaseToken, null); // Test to 'undo' the purchase TEST
}
}
}
// Used to query store and get details of products args include products to query including type and list of SKUs and a listener for response
public void querySkuDetailsAsync(#BillingClient.SkuType final String itemType,
final List<String> skuList, final SkuDetailsResponseListener listener) {
// Create a SkuDetailsParams instance containing args
SkuDetailsParams skuDetailsParams = SkuDetailsParams.newBuilder()
.setSkusList(skuList).setType(itemType).build();
//Query the billing client using the SkuDetailsParams object as an arg
mBillingClient.querySkuDetailsAsync(skuDetailsParams,
new SkuDetailsResponseListener() {
// Override the response to use the listener provided originally in args
#Override
public void onSkuDetailsResponse(int responseCode,
List<SkuDetails> skuDetailsList) {
listener.onSkuDetailsResponse(responseCode, skuDetailsList);
}
});
}
// Start purchase flow with retry option
public void startPurchaseFlow(final String skuId, final String billingType) {
Log.i("dev", "Starting purchaseflow...");
// Specify a runnable to start when connection to Billing client is established
Runnable executeOnConnectedService = new Runnable() {
#Override
public void run() {
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setType(billingType)
.setSku(skuId)
.build();
mBillingClient.launchBillingFlow(mActivity, billingFlowParams);
Log.i("dev", "Just called launchBillingFlow..." + skuId);
}
};
// If Billing client was disconnected, we retry 1 time
// and if success, execute the query
startServiceConnectionIfNeeded(executeOnConnectedService);
}
// Starts connection with reconnect try
private void startServiceConnectionIfNeeded(final Runnable executeOnSuccess) {
if (mBillingClient.isReady()) {
if (executeOnSuccess != null) {
executeOnSuccess.run();
}
} else {
mBillingClient.startConnection(new BillingClientStateListener() {
#Override
public void onBillingSetupFinished(#BillingClient.BillingResponse int billingResponse) {
if (billingResponse == BillingClient.BillingResponse.OK) {
Log.i(TAG, "onBillingSetupFinished() response: " + billingResponse);
if (executeOnSuccess != null) {
executeOnSuccess.run();
}
} else {
Log.w(TAG, "onBillingSetupFinished() error code: " + billingResponse);
}
}
#Override
public void onBillingServiceDisconnected() {
Log.w(TAG, "onBillingServiceDisconnected()");
}
});
}
}
} // End of class
Class that implements interface and initiates request for purchases and displays product information:
public class Store extends AppCompatActivity {
SharedPreferences prefs; // used to access and update the pro value
BillingManager billingManager; // Used to process purchases
// Following are used to store local details about unlock product from the play store
String productSku = "Loading"; // Holds SKU details
String productBillingType = "Loading";
String productTitle = "Loading"; // Will be used to display product title in the store activity
String productPrice = "Loading"; // Used to display product price
String productDescription = "Loading"; // Used to display the product description
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_store);
// Set up toolbar
Toolbar myToolbar = (Toolbar) findViewById(R.id.my_toolbar);
setSupportActionBar(myToolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
// Create billing manager instance
billingManager = new BillingManager(this);
// Set up the shared preferences variable
prefs = this.getSharedPreferences(
"com.identifier", Context.MODE_PRIVATE); // Initiate the preferences
// set up buttons
final Button btnBuy = findViewById(R.id.btnBuy);
btnBuy.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
billingManager.startPurchaseFlow(/*productSku*/ "android.test.purchased", productBillingType); // Amended for TEST
}
});
final Button btnPro = findViewById(R.id.btnPro);
btnPro.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
getProducts();
}
});
getProducts();
updateDisplay();
} // End of onCreate
// Used to unlock the app
public void unlock() {
Log.d("dev", "in unlock(), about to set to true");
prefs.edit().putBoolean("pro", true).apply();
MainActivity.pro = true;
}
// Go back if back/home pressed
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// Respond to the action bar's Up/Home button
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
// Used to request details of products from the store from this class
public void getProducts() {
List<String> inAppSkus = billingManager.getSkus(BillingClient.SkuType.INAPP); // Create local list of Skus for query
billingManager.querySkuDetailsAsync(BillingClient.SkuType.INAPP, inAppSkus, new SkuDetailsResponseListener() {
#Override
public void onSkuDetailsResponse(int responseCode, List<SkuDetails> skuDetailsList) {
if (responseCode == BillingClient.BillingResponse.OK && skuDetailsList != null) {
for (SkuDetails details : skuDetailsList) {
productSku = details.getSku();
productTitle = details.getTitle();
productDescription = details.getDescription();
productPrice = details.getPrice();
productBillingType = details.getType();
}
updateDisplay();
}
}
});
}
// Helper method to update the display with strings
private void updateDisplay() {
final TextView titleText = findViewById(R.id.txtTitle);
final TextView descriptionText = findViewById(R.id.txtDescription);
final TextView priceText = findViewById(R.id.txtPrice);
titleText.setText(productTitle);
descriptionText.setText(productDescription);
priceText.setText(productPrice);
}
}
Ok, so this (replacing the onPurchasesUpdated method above) is now working/responding as expected. Why, I don't know, but it is.
#Override
public void onPurchasesUpdated(#BillingClient.BillingResponse int responseCode,
List<Purchase> purchases) {
if (responseCode == BillingClient.BillingResponse.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
Log.d(TAG, "onPurchasesUpdated() response: " + responseCode);
Log.i("dev", "successful purchase...");
String purchasedSku = purchase.getSku();
Log.i("dev", "Purchased SKU: " + purchasedSku);
String purchaseToken = purchase.getPurchaseToken();
mBillingClient.consumeAsync(purchaseToken, null); // Test to 'undo' the purchase TEST
mActivity.unlock();
}
} else if (responseCode == BillingClient.BillingResponse.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
Log.d(TAG, "onPurchasesUpdated() response: User cancelled" + responseCode);
} else {
// Handle any other error codes.
}
}