blob: ad9c269b1096f115773147a265c370b543af6752 [file] [log] [blame]
/*
* Copyright (C) 2018 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.car.settings.suggestions;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import android.service.settings.suggestions.Suggestion;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.NoSetupPreferenceController;
import com.android.settingslib.suggestions.SuggestionController;
import java.util.ArrayList;
import java.util.List;
/**
* Injects {@link SuggestionPreference} instances loaded from the SuggestionService at the
* location in the hierarchy of the controller's placeholder preference. The placeholder should
* be a {@link Preference} which sets the controller attribute to this name. If the order
* attribute is set, it will be applied to the injected suggestions.
*
* <p>For example:
* <pre>{@code
* <Preference
* android:key="suggestions"
* android:order="100"
* settings:controller="com.android.settings.suggestions.SuggestionsPreferenceController"/>
* }</pre>
*/
public class SuggestionsPreferenceController extends NoSetupPreferenceController implements
LifecycleObserver, SuggestionController.ServiceConnectionListener,
LoaderManager.LoaderCallbacks<List<Suggestion>>, SuggestionPreference.Callback {
private static final Logger LOG = new Logger(SuggestionsPreferenceController.class);
// These values are hard coded until we receive the OK to plumb them through
// SettingsIntelligence. This is ok as right now only SUW uses this framework.
private static final ComponentName COMPONENT_NAME = new ComponentName(
"com.android.settings.intelligence",
"com.android.settings.intelligence.suggestions.SuggestionService");
@VisibleForTesting
SuggestionController mSuggestionController;
private List<Suggestion> mSuggestionsList = new ArrayList<>();
private LoaderManager mLoaderManager;
private PreferenceScreen mPreferenceScreen;
private SuggestionPreferenceGroup mPreferenceGroup;
public SuggestionsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController) {
super(context, preferenceKey, fragmentController);
mSuggestionController = new SuggestionController(context,
COMPONENT_NAME, /* serviceConnectionListener= */ this);
}
public void setLoaderManager(LoaderManager loaderManager) {
mLoaderManager = loaderManager;
}
@Override
public void displayPreference(PreferenceScreen screen) {
Preference placeholder = screen.findPreference(getPreferenceKey());
if (placeholder != null) {
mPreferenceScreen = screen;
mPreferenceGroup = new SuggestionPreferenceGroup(mContext);
mPreferenceGroup.setOrder(placeholder.getOrder());
screen.removePreference(placeholder);
screen.addPreference(mPreferenceGroup);
updateSuggestionPreferences(mSuggestionsList);
} else {
updatePreferenceGroupVisibility();
}
}
/**
* Verifies that the controller was properly initialized with
* {@link #setLoaderManager(LoaderManager)}.
*
* @throws IllegalStateException if the loader manager is {@code null}
*/
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public void checkInitialized() {
LOG.v("checkInitialized");
if (mLoaderManager == null) {
throw new IllegalStateException(
"SuggestionPreferenceController must be initialized by calling "
+ "setLoaderManager(LoaderManager)");
}
}
/** Starts the suggestions controller. */
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void onStart() {
LOG.v("onStart");
mSuggestionController.start();
}
/** Stops the suggestions controller. */
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void onStop() {
LOG.v("onStop");
mSuggestionController.stop();
cleanupLoader();
}
@Override
public void onServiceConnected() {
LOG.v("onServiceConnected");
mLoaderManager.restartLoader(SettingsSuggestionsLoader.LOADER_ID_SUGGESTIONS, /* args= */
null, /* callback= */ this);
}
@Override
public void onServiceDisconnected() {
LOG.v("onServiceDisconnected");
cleanupLoader();
}
@NonNull
@Override
public Loader<List<Suggestion>> onCreateLoader(int id, @Nullable Bundle args) {
LOG.v("onCreateLoader: " + id);
if (id == SettingsSuggestionsLoader.LOADER_ID_SUGGESTIONS) {
return new SettingsSuggestionsLoader(mContext, mSuggestionController);
}
throw new IllegalArgumentException("This loader id is not supported " + id);
}
@Override
public void onLoadFinished(@NonNull Loader<List<Suggestion>> loader,
List<Suggestion> suggestions) {
LOG.v("onLoadFinished");
if (suggestions == null) {
// Load started before the service was ready.
return;
}
updateSuggestionPreferences(suggestions);
mSuggestionsList = new ArrayList<>(suggestions);
}
private void updateSuggestionPreferences(List<Suggestion> suggestions) {
if (mPreferenceGroup == null) {
// Not attached to a hierarchy yet.
return;
}
// Remove suggestions that are not in the new list.
for (Suggestion oldSuggestion : mSuggestionsList) {
boolean isInNewSuggestionList = false;
for (Suggestion suggestion : suggestions) {
if (oldSuggestion.getId().equals(suggestion.getId())) {
isInNewSuggestionList = true;
break;
}
}
if (!isInNewSuggestionList) {
boolean success = mPreferenceGroup.removePreference(
mPreferenceGroup.findPreference(getSuggestionPreferenceKey(oldSuggestion)));
}
}
// Add suggestions that are not in the old list and update the existing suggestions.
for (Suggestion suggestion : suggestions) {
Preference curPref = mPreferenceGroup.findPreference(
getSuggestionPreferenceKey(suggestion));
if (curPref == null) {
SuggestionPreference newSuggPref = new SuggestionPreference(mContext,
suggestion, /* callback= */ this);
mPreferenceGroup.addPreference(newSuggPref);
} else {
((SuggestionPreference) curPref).updateSuggestion(suggestion);
}
}
updatePreferenceGroupVisibility();
}
@Override
public void onLoaderReset(@NonNull Loader<List<Suggestion>> loader) {
LOG.v("onLoaderReset");
}
@Override
public void launchSuggestion(SuggestionPreference preference) {
LOG.v("launchSuggestion");
Suggestion suggestion = preference.getSuggestion();
try {
if (suggestion.getPendingIntent() != null) {
suggestion.getPendingIntent().send();
mSuggestionController.launchSuggestion(suggestion);
} else {
LOG.w("Suggestion with null pending intent " + suggestion.getId());
}
} catch (PendingIntent.CanceledException e) {
LOG.w("Failed to start suggestion " + suggestion.getId());
}
}
@Override
public void dismissSuggestion(SuggestionPreference preference) {
LOG.v("dismissSuggestion");
Suggestion suggestion = preference.getSuggestion();
mSuggestionController.dismissSuggestions(suggestion);
mSuggestionsList.remove(suggestion);
mPreferenceGroup.removePreference(preference);
updatePreferenceGroupVisibility();
}
private void updatePreferenceGroupVisibility() {
if (mPreferenceGroup == null) {
return;
}
boolean addToScreen = isAvailable() && mPreferenceGroup.getPreferenceCount() > 0;
if (addToScreen) {
mPreferenceScreen.addPreference(mPreferenceGroup);
} else {
mPreferenceScreen.removePreference(mPreferenceGroup);
}
}
private void cleanupLoader() {
LOG.v("cleanupLoader");
mLoaderManager.destroyLoader(SettingsSuggestionsLoader.LOADER_ID_SUGGESTIONS);
}
private String getSuggestionPreferenceKey(Suggestion suggestion) {
return SuggestionPreference.SUGGESTION_PREFERENCE_KEY + suggestion.getId();
}
/** Concrete preference group with a layout that removes unused views. */
private class SuggestionPreferenceGroup extends PreferenceGroup {
SuggestionPreferenceGroup(Context context) {
super(context, /* attrs= */ null);
setLayoutResource(R.layout.suggestion_preference_group);
}
}
}