Merge "[Reskin] Replace string for chargin on hold" into main
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9b64fc4..4230b6a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4697,14 +4697,6 @@
<receiver android:name=".fuelgauge.batterytip.AnomalyDetectionReceiver"
android:exported="false" />
- <receiver android:name=".fuelgauge.batterytip.AnomalyConfigReceiver"
- android:exported="true">
- <intent-filter>
- <action android:name="android.app.action.STATSD_STARTED"/>
- <action android:name="android.intent.action.BOOT_COMPLETED"/>
- </intent-filter>
- </receiver>
-
<service android:name=".fuelgauge.batterytip.AnomalyCleanupJobService"
android:permission="android.permission.BIND_JOB_SERVICE" />
diff --git a/res/drawable/ic_audio_play_sample.xml b/res/drawable/ic_audio_play_sample.xml
new file mode 100644
index 0000000..3666c22
--- /dev/null
+++ b/res/drawable/ic_audio_play_sample.xml
@@ -0,0 +1,32 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/colorControlNormal">
+ <path
+ android:pathData="M14,8C9.6,8 6,11.6 6,16H8C8,12.7 10.7,10 14,10V8Z"
+ android:fillColor="#4E4639"/>
+ <path
+ android:pathData="M14,6V4C7.4,4 2,9.4 2,16H4C4,10.5 8.5,6 14,6Z"
+ android:fillColor="#4E4639"/>
+ <path
+ android:pathData="M16,4V12.6C15.4,12.3 14.7,12 14,12C11.8,12 10,13.8 10,16C10,18.2 11.8,20 14,20C16.2,20 18,18.2 18,16V7H22V4H16ZM14,18C12.9,18 12,17.1 12,16C12,14.9 12.9,14 14,14C15.1,14 16,14.9 16,16C16,17.1 15.1,18 14,18Z"
+ android:fillColor="#4E4639"/>
+</vector>
diff --git a/res/values/config.xml b/res/values/config.xml
index 6f784dd..f3e2a7a 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -793,4 +793,8 @@
<!-- Whether to display pSIM conversion menu in Settings.-->
<bool name="config_psim_conversion_menu_enabled">false</bool>
+
+ <!-- Array of carrier id to allow the pSIM conversion-->
+ <integer-array name="config_psim_conversion_menu_enabled_carrier" translatable="false">
+ </integer-array>
</resources>
diff --git a/res/xml/bluetooth_audio_sharing.xml b/res/xml/bluetooth_audio_sharing.xml
index d5e08bb..9ffa2b2 100644
--- a/res/xml/bluetooth_audio_sharing.xml
+++ b/res/xml/bluetooth_audio_sharing.xml
@@ -31,6 +31,13 @@
android:title="@string/calls_and_alarms_device_title"
settings:controller="com.android.settings.connecteddevice.audiosharing.CallsAndAlarmsPreferenceController" />
+ <Preference
+ android:icon="@drawable/ic_audio_play_sample"
+ android:key="audio_sharing_play_sound"
+ android:summary="Everyone listening should hear it"
+ android:title="Play a test sound"
+ settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPlaySoundPreferenceController" />
+
<com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreference
android:key="audio_sharing_stream_name"
android:summary="********"
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
index 52a8f18..9105297 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
@@ -34,6 +34,7 @@
private AudioSharingSwitchBarController mSwitchBarController;
private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController;
private CallsAndAlarmsPreferenceController mCallsAndAlarmsPreferenceController;
+ private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController;
private AudioSharingNamePreferenceController mAudioSharingNamePreferenceController;
private AudioStreamsCategoryController mAudioStreamsCategoryController;
@@ -74,6 +75,8 @@
mAudioSharingDeviceVolumeGroupController.init(this);
mCallsAndAlarmsPreferenceController = use(CallsAndAlarmsPreferenceController.class);
mCallsAndAlarmsPreferenceController.init(this);
+ mAudioSharingPlaySoundPreferenceController =
+ use(AudioSharingPlaySoundPreferenceController.class);
mAudioSharingNamePreferenceController = use(AudioSharingNamePreferenceController.class);
mAudioStreamsCategoryController = use(AudioStreamsCategoryController.class);
}
@@ -100,6 +103,7 @@
private void updateVisibilityForAttachedPreferences() {
mAudioSharingDeviceVolumeGroupController.updateVisibility();
mCallsAndAlarmsPreferenceController.updateVisibility();
+ mAudioSharingPlaySoundPreferenceController.updateVisibility();
mAudioSharingNamePreferenceController.updateVisibility();
mAudioStreamsCategoryController.updateVisibility();
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPlaySoundPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPlaySoundPreferenceController.java
new file mode 100644
index 0000000..6722219
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPlaySoundPreferenceController.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+
+public class AudioSharingPlaySoundPreferenceController
+ extends AudioSharingBasePreferenceController {
+
+ private static final String TAG = "AudioSharingPlaySoundPreferenceController";
+
+ private static final String PREF_KEY = "audio_sharing_play_sound";
+
+ private final Ringtone mRingtone;
+
+ public AudioSharingPlaySoundPreferenceController(Context context) {
+ super(context, PREF_KEY);
+ mRingtone = RingtoneManager.getRingtone(context, getMediaVolumeUri());
+ if (mRingtone != null) {
+ mRingtone.setStreamType(AudioManager.STREAM_MUSIC);
+ }
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPreference.setVisible(mRingtone != null);
+ mPreference.setOnPreferenceClickListener(
+ (v) -> {
+ if (mRingtone == null) {
+ Log.d(TAG, "Skip onClick due to ringtone is null");
+ return true;
+ }
+ try {
+ mRingtone.setAudioAttributes(
+ new AudioAttributes.Builder(mRingtone.getAudioAttributes())
+ .setFlags(AudioAttributes.FLAG_BYPASS_MUTE)
+ .addTag("VX_AOSP_SAMPLESOUND")
+ .build());
+ if (!mRingtone.isPlaying()) {
+ mRingtone.play();
+ }
+ } catch (Throwable e) {
+ Log.w(TAG, "Fail to play sample, error = " + e);
+ }
+ return true;
+ });
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ super.onStop(owner);
+ if (mRingtone != null && mRingtone.isPlaying()) {
+ mRingtone.stop();
+ }
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return PREF_KEY;
+ }
+
+ private Uri getMediaVolumeUri() {
+ return Uri.parse(
+ ContentResolver.SCHEME_ANDROID_RESOURCE
+ + "://"
+ + mContext.getPackageName()
+ + "/"
+ + R.raw.media_volume);
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
index ffb0b88..678f952 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
@@ -35,21 +35,27 @@
*/
class AudioStreamPreference extends TwoTargetPreference {
private boolean mIsConnected = false;
+ private AudioStream mAudioStream;
/**
* Update preference UI based on connection status
*
- * @param isConnected Is this streams connected
+ * @param isConnected Is this stream connected
+ * @param summary Summary text
+ * @param onPreferenceClickListener Click listener for the preference
*/
void setIsConnected(
- boolean isConnected, @Nullable OnPreferenceClickListener onPreferenceClickListener) {
+ boolean isConnected,
+ String summary,
+ @Nullable OnPreferenceClickListener onPreferenceClickListener) {
if (mIsConnected == isConnected
+ && getSummary() == summary
&& getOnPreferenceClickListener() == onPreferenceClickListener) {
// Nothing to update.
return;
}
mIsConnected = isConnected;
- setSummary(isConnected ? "Listening now" : "");
+ setSummary(summary);
setOrder(isConnected ? 0 : 1);
setOnPreferenceClickListener(onPreferenceClickListener);
notifyChanged();
@@ -60,6 +66,14 @@
setIcon(R.drawable.ic_bt_audio_sharing);
}
+ void setAudioStreamState(AudioStreamsProgressCategoryController.AudioStreamState state) {
+ mAudioStream.setState(state);
+ }
+
+ AudioStreamsProgressCategoryController.AudioStreamState getAudioStreamState() {
+ return mAudioStream.getState();
+ }
+
@Override
protected boolean shouldHideSecondTarget() {
return mIsConnected;
@@ -71,19 +85,31 @@
}
static AudioStreamPreference fromMetadata(
- Context context, BluetoothLeBroadcastMetadata source) {
+ Context context,
+ BluetoothLeBroadcastMetadata source,
+ AudioStreamsProgressCategoryController.AudioStreamState streamState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(source));
+ preference.setAudioStream(new AudioStream(source.getBroadcastId(), streamState));
return preference;
}
static AudioStreamPreference fromReceiveState(
- Context context, BluetoothLeBroadcastReceiveState state) {
+ Context context,
+ BluetoothLeBroadcastReceiveState receiveState,
+ AudioStreamsProgressCategoryController.AudioStreamState streamState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
- preference.setTitle(getBroadcastName(state));
+ preference.setTitle(getBroadcastName(receiveState));
+ preference.setAudioStream(
+ new AudioStream(
+ receiveState.getSourceId(), receiveState.getBroadcastId(), streamState));
return preference;
}
+ private void setAudioStream(AudioStream audioStream) {
+ mAudioStream = audioStream;
+ }
+
private static String getBroadcastName(BluetoothLeBroadcastMetadata source) {
return source.getSubgroups().stream()
.map(s -> s.getContentMetadata().getProgramInfo())
@@ -99,4 +125,43 @@
.findFirst()
.orElse("Broadcast Id: " + state.getBroadcastId());
}
+
+ private static final class AudioStream {
+ private int mSourceId;
+ private int mBroadcastId;
+ private AudioStreamsProgressCategoryController.AudioStreamState mState;
+
+ private AudioStream(
+ int broadcastId, AudioStreamsProgressCategoryController.AudioStreamState state) {
+ mBroadcastId = broadcastId;
+ mState = state;
+ }
+
+ private AudioStream(
+ int sourceId,
+ int broadcastId,
+ AudioStreamsProgressCategoryController.AudioStreamState state) {
+ mSourceId = sourceId;
+ mBroadcastId = broadcastId;
+ mState = state;
+ }
+
+ // TODO(chelseahao): use this to handleSourceRemoved
+ private int getSourceId() {
+ return mSourceId;
+ }
+
+ // TODO(chelseahao): use this to handleSourceRemoved
+ private int getBroadcastId() {
+ return mBroadcastId;
+ }
+
+ private AudioStreamsProgressCategoryController.AudioStreamState getState() {
+ return mState;
+ }
+
+ private void setState(AudioStreamsProgressCategoryController.AudioStreamState state) {
+ mState = state;
+ }
+ }
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
index a418415..b0af7dd 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
@@ -34,7 +34,7 @@
public class AudioStreamsDashboardFragment extends DashboardFragment {
private static final String TAG = "AudioStreamsDashboardFrag";
private static final boolean DEBUG = BluetoothUtils.D;
- private AudioStreamsScanQrCodeController mAudioStreamsScanQrCodeController;
+ private AudioStreamsProgressCategoryController mAudioStreamsProgressCategoryController;
public AudioStreamsDashboardFragment() {
super();
@@ -69,8 +69,8 @@
@Override
public void onAttach(Context context) {
super.onAttach(context);
- mAudioStreamsScanQrCodeController = use(AudioStreamsScanQrCodeController.class);
- mAudioStreamsScanQrCodeController.setFragment(this);
+ use(AudioStreamsScanQrCodeController.class).setFragment(this);
+ mAudioStreamsProgressCategoryController = use(AudioStreamsProgressCategoryController.class);
}
@Override
@@ -103,11 +103,13 @@
if (DEBUG) {
Log.d(TAG, "onActivityResult() broadcastId : " + source.getBroadcastId());
}
- if (mAudioStreamsScanQrCodeController == null) {
- Log.w(TAG, "onActivityResult() AudioStreamsScanQrCodeController is null!");
+ if (mAudioStreamsProgressCategoryController == null) {
+ Log.w(
+ TAG,
+ "onActivityResult() AudioStreamsProgressCategoryController is null!");
return;
}
- mAudioStreamsScanQrCodeController.addSource(source);
+ mAudioStreamsProgressCategoryController.setSourceFromQrCode(source);
}
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
index 198e8e5..2c6eedb 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
@@ -109,13 +109,14 @@
}
/** Retrieves a list of all LE broadcast receive states from active sinks. */
- List<BluetoothLeBroadcastReceiveState> getAllSources() {
+ List<BluetoothLeBroadcastReceiveState> getAllConnectedSources() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
return emptyList();
}
return getActiveSinksOnAssistant(mBluetoothManager).stream()
.flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
+ .filter(this::isConnected)
.toList();
}
@@ -124,7 +125,7 @@
return mLeBroadcastAssistant;
}
- static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
+ boolean isConnected(BluetoothLeBroadcastReceiveState state) {
return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
&& state.getBigEncryptionState()
== BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index 3c005b2..ab380c8 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -25,6 +25,7 @@
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Bundle;
+import android.os.CountDownTimer;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -71,6 +72,17 @@
}
};
+ enum AudioStreamState {
+ // When mTimedSourceFromQrCode is present and this source has not been synced.
+ WAIT_FOR_SYNC,
+ // When source has been synced but not added to any sink.
+ SYNCED,
+ // When addSource is called for this source and waiting for response.
+ WAIT_FOR_SOURCE_ADD,
+ // Source is added to active sink.
+ SOURCE_ADDED,
+ }
+
private final Executor mExecutor;
private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
private final AudioStreamsHelper mAudioStreamsHelper;
@@ -78,6 +90,7 @@
private final @Nullable LocalBluetoothManager mBluetoothManager;
private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>();
+ private TimedSourceFromQrCode mTimedSourceFromQrCode;
private AudioStreamsProgressCategoryPreference mCategoryPreference;
public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
@@ -122,6 +135,12 @@
mExecutor.execute(this::stopScanning);
}
+ void setSourceFromQrCode(BluetoothLeBroadcastMetadata source) {
+ mTimedSourceFromQrCode =
+ new TimedSourceFromQrCode(
+ mContext, source, () -> handleSourceLost(source.getBroadcastId()));
+ }
+
void setScanning(boolean isScanning) {
ThreadUtils.postOnMainThread(
() -> {
@@ -140,24 +159,90 @@
}
if (source.isEncrypted()) {
ThreadUtils.postOnMainThread(
- () -> launchPasswordDialog(source, preference));
+ () ->
+ launchPasswordDialog(
+ source, (AudioStreamPreference) preference));
} else {
mAudioStreamsHelper.addSource(source);
+ ((AudioStreamPreference) preference)
+ .setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
+ updatePreferenceConnectionState(
+ (AudioStreamPreference) preference,
+ AudioStreamState.WAIT_FOR_SOURCE_ADD,
+ null);
}
return true;
};
- mBroadcastIdToPreferenceMap.computeIfAbsent(
- source.getBroadcastId(),
- k -> {
- var preference = AudioStreamPreference.fromMetadata(mContext, source);
- ThreadUtils.postOnMainThread(
- () -> {
- preference.setIsConnected(false, addSourceOrShowDialog);
- if (mCategoryPreference != null) {
- mCategoryPreference.addPreference(preference);
- }
- });
- return preference;
+
+ var broadcastIdFound = source.getBroadcastId();
+ mBroadcastIdToPreferenceMap.compute(
+ broadcastIdFound,
+ (k, v) -> {
+ if (v == null) {
+ return addNewPreference(
+ source, AudioStreamState.SYNCED, addSourceOrShowDialog);
+ }
+ var fromState = v.getAudioStreamState();
+ if (fromState == AudioStreamState.WAIT_FOR_SYNC) {
+ var pendingSource = mTimedSourceFromQrCode.get();
+ if (pendingSource == null) {
+ Log.w(
+ TAG,
+ "handleSourceFound(): unexpected state with null pendingSource:"
+ + fromState
+ + " for broadcastId : "
+ + broadcastIdFound);
+ v.setAudioStreamState(AudioStreamState.SYNCED);
+ return v;
+ }
+ mAudioStreamsHelper.addSource(pendingSource);
+ mTimedSourceFromQrCode.consumed();
+ v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
+ updatePreferenceConnectionState(
+ v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ } else {
+ if (fromState != AudioStreamState.SOURCE_ADDED) {
+ Log.w(
+ TAG,
+ "handleSourceFound(): unexpected state : "
+ + fromState
+ + " for broadcastId : "
+ + broadcastIdFound);
+ }
+ }
+ return v;
+ });
+ }
+
+ private void handleSourceFromQrCodeIfExists() {
+ if (mTimedSourceFromQrCode == null || mTimedSourceFromQrCode.get() == null) {
+ return;
+ }
+ var metadataFromQrCode = mTimedSourceFromQrCode.get();
+ mBroadcastIdToPreferenceMap.compute(
+ metadataFromQrCode.getBroadcastId(),
+ (k, v) -> {
+ if (v == null) {
+ mTimedSourceFromQrCode.waitForConsume();
+ return addNewPreference(
+ metadataFromQrCode, AudioStreamState.WAIT_FOR_SYNC, null);
+ }
+ var fromState = v.getAudioStreamState();
+ if (fromState == AudioStreamState.SYNCED) {
+ mAudioStreamsHelper.addSource(metadataFromQrCode);
+ mTimedSourceFromQrCode.consumed();
+ v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
+ updatePreferenceConnectionState(
+ v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ } else {
+ Log.w(
+ TAG,
+ "handleSourceFromQrCode(): unexpected state : "
+ + fromState
+ + " for broadcastId : "
+ + metadataFromQrCode.getBroadcastId());
+ }
+ return v;
});
}
@@ -174,32 +259,54 @@
mAudioStreamsHelper.removeSource(broadcastId);
}
- void handleSourceConnected(BluetoothLeBroadcastReceiveState state) {
- if (!AudioStreamsHelper.isConnected(state)) {
+ void handleSourceConnected(BluetoothLeBroadcastReceiveState receiveState) {
+ if (!mAudioStreamsHelper.isConnected(receiveState)) {
return;
}
+ var sourceAddedState = AudioStreamState.SOURCE_ADDED;
+ var broadcastIdConnected = receiveState.getBroadcastId();
mBroadcastIdToPreferenceMap.compute(
- state.getBroadcastId(),
+ broadcastIdConnected,
(k, v) -> {
- // True if this source has been added either by scanning, or it's currently
- // connected to another active sink.
- boolean existed = v != null;
- AudioStreamPreference preference =
- existed ? v : AudioStreamPreference.fromReceiveState(mContext, state);
-
- ThreadUtils.postOnMainThread(
- () -> {
- preference.setIsConnected(
- true, p -> launchDetailFragment(state.getBroadcastId()));
- if (mCategoryPreference != null && !existed) {
- mCategoryPreference.addPreference(preference);
- }
- });
-
- return preference;
+ if (v == null) {
+ return addNewPreference(
+ receiveState,
+ sourceAddedState,
+ p -> launchDetailFragment(broadcastIdConnected));
+ }
+ var fromState = v.getAudioStreamState();
+ if (fromState == AudioStreamState.WAIT_FOR_SOURCE_ADD
+ || fromState == AudioStreamState.SYNCED
+ || fromState == AudioStreamState.WAIT_FOR_SYNC) {
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.consumed();
+ }
+ } else {
+ if (fromState != AudioStreamState.SOURCE_ADDED) {
+ Log.w(
+ TAG,
+ "handleSourceConnected(): unexpected state : "
+ + fromState
+ + " for broadcastId : "
+ + broadcastIdConnected);
+ }
+ }
+ v.setAudioStreamState(sourceAddedState);
+ updatePreferenceConnectionState(
+ v, sourceAddedState, p -> launchDetailFragment(broadcastIdConnected));
+ return v;
});
}
+ private static String getPreferenceSummary(AudioStreamState state) {
+ return switch (state) {
+ case WAIT_FOR_SYNC -> "Scanning...";
+ case WAIT_FOR_SOURCE_ADD -> "Connecting...";
+ case SOURCE_ADDED -> "Listening now";
+ default -> "";
+ };
+ }
+
void showToast(String msg) {
AudioSharingUtils.toastMessage(mContext, msg);
}
@@ -235,13 +342,15 @@
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mLeBroadcastAssistant.startSearchingForSources(emptyList());
- // Display currently connected streams
+ // Handle QR code scan and display currently connected streams
var unused =
ThreadUtils.postOnBackgroundThread(
- () ->
- mAudioStreamsHelper
- .getAllSources()
- .forEach(this::handleSourceConnected));
+ () -> {
+ handleSourceFromQrCodeIfExists();
+ mAudioStreamsHelper
+ .getAllConnectedSources()
+ .forEach(this::handleSourceConnected);
+ });
}
private void stopScanning() {
@@ -256,6 +365,43 @@
mLeBroadcastAssistant.stopSearchingForSources();
}
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.consumed();
+ }
+ }
+
+ private AudioStreamPreference addNewPreference(
+ BluetoothLeBroadcastReceiveState receiveState,
+ AudioStreamState state,
+ Preference.OnPreferenceClickListener onClickListener) {
+ var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState, state);
+ updatePreferenceConnectionState(preference, state, onClickListener);
+ return preference;
+ }
+
+ private AudioStreamPreference addNewPreference(
+ BluetoothLeBroadcastMetadata metadata,
+ AudioStreamState state,
+ Preference.OnPreferenceClickListener onClickListener) {
+ var preference = AudioStreamPreference.fromMetadata(mContext, metadata, state);
+ updatePreferenceConnectionState(preference, state, onClickListener);
+ return preference;
+ }
+
+ private void updatePreferenceConnectionState(
+ AudioStreamPreference preference,
+ AudioStreamState state,
+ Preference.OnPreferenceClickListener onClickListener) {
+ ThreadUtils.postOnMainThread(
+ () -> {
+ preference.setIsConnected(
+ state == AudioStreamState.SOURCE_ADDED,
+ getPreferenceSummary(state),
+ onClickListener);
+ if (mCategoryPreference != null) {
+ mCategoryPreference.addPreference(preference);
+ }
+ });
}
private boolean launchDetailFragment(int broadcastId) {
@@ -282,7 +428,8 @@
return true;
}
- private void launchPasswordDialog(BluetoothLeBroadcastMetadata source, Preference preference) {
+ private void launchPasswordDialog(
+ BluetoothLeBroadcastMetadata source, AudioStreamPreference preference) {
View layout =
LayoutInflater.from(mContext)
.inflate(R.layout.bluetooth_find_broadcast_password_dialog, null);
@@ -307,8 +454,49 @@
.setBroadcastCode(
code.getBytes(StandardCharsets.UTF_8))
.build());
+ preference.setAudioStreamState(
+ AudioStreamState.WAIT_FOR_SOURCE_ADD);
+ updatePreferenceConnectionState(
+ preference, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
})
.create();
alertDialog.show();
}
+
+ private static class TimedSourceFromQrCode {
+ private static final int WAIT_FOR_SYNC_TIMEOUT_MILLIS = 15000;
+ private final CountDownTimer mTimer;
+ private BluetoothLeBroadcastMetadata mSourceFromQrCode;
+
+ private TimedSourceFromQrCode(
+ Context context,
+ BluetoothLeBroadcastMetadata sourceFromQrCode,
+ Runnable timeoutAction) {
+ mSourceFromQrCode = sourceFromQrCode;
+ mTimer =
+ new CountDownTimer(WAIT_FOR_SYNC_TIMEOUT_MILLIS, 1000) {
+ @Override
+ public void onTick(long millisUntilFinished) {}
+
+ @Override
+ public void onFinish() {
+ timeoutAction.run();
+ AudioSharingUtils.toastMessage(context, "Audio steam isn't available");
+ }
+ };
+ }
+
+ private void waitForConsume() {
+ mTimer.start();
+ }
+
+ private void consumed() {
+ mTimer.cancel();
+ mSourceFromQrCode = null;
+ }
+
+ private BluetoothLeBroadcastMetadata get() {
+ return mSourceFromQrCode;
+ }
+ }
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java b/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java
index b1018ba..a80987d 100644
--- a/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java
+++ b/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java
@@ -49,6 +49,7 @@
import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
import com.android.settingslib.utils.ThreadUtils;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -59,7 +60,7 @@
private static final int ON = 1;
@VisibleForTesting static final int UID_NULL = -1;
@VisibleForTesting static final int STATSD_UID_FILED = 1;
- @VisibleForTesting static final long MAX_DELAY_MS = TimeUnit.MINUTES.toMillis(30);
+ @VisibleForTesting static final long MAX_DELAY_MS = Duration.ofDays(1).toMillis();
private final Object mLock = new Object();
diff --git a/src/com/android/settings/network/apn/ApnEditPageProvider.kt b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
index 52066a1..2600618 100644
--- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt
+++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
@@ -101,17 +101,19 @@
RegularScaffold(
title = if (apnDataInit.newApn) stringResource(id = R.string.apn_add) else stringResource(id = R.string.apn_edit),
actions = {
- IconButton(onClick = {
- if (!apnData.validEnabled) apnData = apnData.copy(validEnabled = true)
- val valid = validateAndSaveApnData(
- apnDataInit,
- apnData,
- context,
- uriInit,
- networkTypeSelectedOptionsState
- )
- if (valid) navController.navigateBack()
- }) { Icon(imageVector = Icons.Outlined.Done, contentDescription = null) }
+ if (!apnData.customizedConfig.readOnlyApn) {
+ IconButton(onClick = {
+ if (!apnData.validEnabled) apnData = apnData.copy(validEnabled = true)
+ val valid = validateAndSaveApnData(
+ apnDataInit,
+ apnData,
+ context,
+ uriInit,
+ networkTypeSelectedOptionsState
+ )
+ if (valid) navController.navigateBack()
+ }) { Icon(imageVector = Icons.Outlined.Done, contentDescription = null) }
+ }
},
) {
Column {
@@ -212,7 +214,9 @@
emptyVal = stringResource(R.string.network_type_unspecified),
enabled = apnData.networkTypeEnabled
) {}
- if (!apnData.newApn) {
+ if (!apnData.newApn && !apnData.customizedConfig.readOnlyApn
+ && apnData.customizedConfig.isAddApnAllowed
+ ) {
Preference(
object : PreferenceModel {
override val title = stringResource(R.string.menu_delete)
diff --git a/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java b/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java
index 27b8c16..441c249 100644
--- a/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java
+++ b/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java
@@ -51,6 +51,7 @@
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
public class ConvertToEsimPreferenceController extends TelephonyBasePreferenceController implements
@@ -111,7 +112,8 @@
* To avoid showing users dialogs that can cause confusion,
* add conditions to allow conversion in the absence of active eSIM.
*/
- if (!mContext.getResources().getBoolean(R.bool.config_psim_conversion_menu_enabled)) {
+ if (!mContext.getResources().getBoolean(R.bool.config_psim_conversion_menu_enabled)
+ || !isPsimConversionSupport(subId)) {
return CONDITIONALLY_UNAVAILABLE;
}
if (findConversionSupportComponent()) {
@@ -238,4 +240,16 @@
}
return true;
}
+
+ private boolean isPsimConversionSupport(int subId) {
+ SubscriptionManager subscriptionManager = mContext.getSystemService(
+ SubscriptionManager.class);
+ SubscriptionInfo subInfo = subscriptionManager.getActiveSubscriptionInfo(subId);
+ if (subInfo == null) {
+ return false;
+ }
+ final int[] supportedCarriers = mContext.getResources().getIntArray(
+ R.array.config_psim_conversion_menu_enabled_carrier);
+ return Arrays.stream(supportedCarriers).anyMatch(id -> id == subInfo.getCarrierId());
+ }
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobServiceTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobServiceTest.java
index a67e5d3..482f0d0 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobServiceTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobServiceTest.java
@@ -71,6 +71,7 @@
import org.robolectric.android.controller.ServiceController;
import org.robolectric.annotation.Config;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -133,7 +134,7 @@
JobInfo pendingJob = pendingJobs.get(0);
assertThat(pendingJob.getId()).isEqualTo(R.integer.job_anomaly_detection);
assertThat(pendingJob.getMaxExecutionDelayMillis())
- .isEqualTo(TimeUnit.MINUTES.toMillis(30));
+ .isEqualTo(Duration.ofDays(1).toMillis());
}
@Test
diff --git a/tests/robotests/src/com/android/settings/widget/LinkifySummaryPreferenceTest.java b/tests/robotests/src/com/android/settings/widget/LinkifySummaryPreferenceTest.java
index f060588..b33a564 100644
--- a/tests/robotests/src/com/android/settings/widget/LinkifySummaryPreferenceTest.java
+++ b/tests/robotests/src/com/android/settings/widget/LinkifySummaryPreferenceTest.java
@@ -32,21 +32,22 @@
import android.widget.TextView;
import androidx.preference.PreferenceViewHolder;
+import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
-import org.junit.Ignore;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-@Ignore("b/313563183")
@RunWith(RobolectricTestRunner.class)
public class LinkifySummaryPreferenceTest {
- @Spy
+ @Rule
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
private PreferenceViewHolder mViewHolder;
@Mock
private TextView mSummaryTextView;
@@ -54,9 +55,7 @@
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- final Context context = RuntimeEnvironment.application;
+ final Context context = ApplicationProvider.getApplicationContext();
mPreference = new LinkifySummaryPreference(context, null /* attrs */);
final View view = spy(View.inflate(context, mPreference.getLayoutResource(),