2013-12-14 11 views
15

Faccio una domanda per e sto usando CursorTreeAdapter come ExpandableListView. Ora voglio usare la casella di ricerca per visualizzare gli elementi ExpandableListView filtrati. Come questo: http://i.imgur.com/8ua7Mkl.pngCursorTreeAdapter con implementazione ricerca

Ecco il codice di quello che ho finora:

MainActivity.java:

package com.example.cursortreeadaptersearch; 

import java.util.HashMap; 

import android.app.SearchManager; 
import android.content.Context; 
import android.database.ContentObserver; 
import android.database.Cursor; 
import android.net.Uri; 
import android.os.Bundle; 
import android.os.Handler; 
import android.provider.ContactsContract; 
import android.support.v4.app.LoaderManager; 
import android.support.v4.app.LoaderManager.LoaderCallbacks; 
import android.support.v4.content.CursorLoader; 
import android.support.v4.content.Loader; 
import android.util.Log; 
import android.widget.ExpandableListView; 
import android.widget.SearchView; 
import android.widget.SearchView.OnCloseListener; 
import android.widget.SearchView.OnQueryTextListener; 

import com.actionbarsherlock.app.SherlockFragmentActivity; 

public class MainActivity extends SherlockFragmentActivity { 

    private SearchView search; 
    private MyListAdapter listAdapter; 
    private ExpandableListView myList; 

    private final String DEBUG_TAG = getClass().getSimpleName().toString(); 

    /** 
    * The columns we are interested in from the database 
    */ 
    static final String[] CONTACTS_PROJECTION = new String[] { 
      ContactsContract.Contacts._ID, 
      ContactsContract.Contacts.DISPLAY_NAME, 
      ContactsContract.Contacts.PHOTO_ID, 
      ContactsContract.CommonDataKinds.Email.DATA, 
      ContactsContract.CommonDataKinds.Photo.CONTACT_ID }; 

    static final String[] GROUPS_SUMMARY_PROJECTION = new String[] { 
      ContactsContract.Groups.TITLE, ContactsContract.Groups._ID, 
      ContactsContract.Groups.SUMMARY_COUNT, 
      ContactsContract.Groups.ACCOUNT_NAME, 
      ContactsContract.Groups.ACCOUNT_TYPE, 
      ContactsContract.Groups.DATA_SET }; 

    @Override 
    public void onCreate(Bundle savedInstanceState) { 
     super.onCreate(savedInstanceState); 
     setContentView(R.layout.activity_main); 

     SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); 
     search = (SearchView) findViewById(R.id.search); 
     search.setSearchableInfo(searchManager 
       .getSearchableInfo(getComponentName())); 
     search.setIconifiedByDefault(false); 
     search.setOnQueryTextListener(new OnQueryTextListener() { 

      @Override 
      public boolean onQueryTextSubmit(String query) { 
       listAdapter.filterList(query); 
       expandAll(); 
       return false; 
      } 

      @Override 
      public boolean onQueryTextChange(String query) { 
       listAdapter.filterList(query); 
       expandAll(); 
       return false; 
      } 
     }); 

     search.setOnCloseListener(new OnCloseListener() { 

      @Override 
      public boolean onClose() { 
       listAdapter.filterList(""); 
       expandAll(); 
       return false; 
      } 
     }); 

     // get reference to the ExpandableListView 
     myList = (ExpandableListView) findViewById(R.id.expandableList); 
     // create the adapter 
     listAdapter = new MyListAdapter(null, MainActivity.this); 
     // attach the adapter to the list 
     myList.setAdapter(listAdapter); 

     Loader<Cursor> loader = getSupportLoaderManager().getLoader(-1); 
     if (loader != null && !loader.isReset()) { 
      runOnUiThread(new Runnable() { 
       public void run() { 
        getSupportLoaderManager().restartLoader(-1, null, 
          mSpeakersLoaderCallback); 
       } 
      }); 
     } else { 
      runOnUiThread(new Runnable() { 
       public void run() { 
        getSupportLoaderManager().initLoader(-1, null, 
          mSpeakersLoaderCallback).forceLoad(); 
        ; 
       } 
      }); 
     } 

    } 

    @Override 
    public void onResume() { 
     super.onResume(); 

     getApplicationContext().getContentResolver().registerContentObserver(
       ContactsContract.Data.CONTENT_URI, true, 
       mSpeakerChangesObserver); 
    } 

    @Override 
    public void onPause() { 
     super.onPause(); 

     getApplicationContext().getContentResolver().unregisterContentObserver(
       mSpeakerChangesObserver); 
    } 

    // method to expand all groups 
    private void expandAll() { 
     int count = listAdapter.getGroupCount(); 
     for (int i = 0; i < count; i++) { 
      myList.expandGroup(i); 
     } 
    } 

    public LoaderManager.LoaderCallbacks<Cursor> mSpeakersLoaderCallback = new LoaderCallbacks<Cursor>() { 

     @Override 
     public Loader<Cursor> onCreateLoader(int id, Bundle args) { 
      Log.d(DEBUG_TAG, "onCreateLoader for loader_id " + id); 
      CursorLoader cl = null; 

      HashMap<Integer, Integer> groupMap = listAdapter.getGroupMap(); 
      if (id != -1) { 
       int groupPos = groupMap.get(id); 
       if (groupPos == 0) { // E-mail group 
        String[] PROJECTION = new String[] { 
          ContactsContract.RawContacts._ID, 
          ContactsContract.CommonDataKinds.Email.DATA }; 
        String sortOrder = "CASE WHEN " 
          + ContactsContract.Contacts.DISPLAY_NAME 
          + " NOT LIKE '%@%' THEN 1 ELSE 2 END, " 
          + ContactsContract.Contacts.DISPLAY_NAME + ", " 
          + ContactsContract.CommonDataKinds.Email.DATA 
          + " COLLATE NOCASE"; 
        String selection = ContactsContract.CommonDataKinds.Email.DATA 
          + " NOT LIKE ''"; 
        cl = new CursorLoader(getApplicationContext(), 
          ContactsContract.CommonDataKinds.Email.CONTENT_URI, 
          PROJECTION, selection, null, sortOrder); 
       } else if (groupPos == 1) { // Name group 
        Uri contactsUri = ContactsContract.Data.CONTENT_URI; 
        String selection = "((" 
          + ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME 
          + " NOTNULL) AND (" 
          + ContactsContract.CommonDataKinds.GroupMembership.HAS_PHONE_NUMBER 
          + "=1) AND (" 
          + ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME 
          + " != '') AND (" 
          + ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID 
          + " = '1'))"; // Row ID 1 == All contacts 
        String sortOrder = ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME 
          + " COLLATE LOCALIZED ASC"; 

        cl = new CursorLoader(getApplicationContext(), contactsUri, 
          CONTACTS_PROJECTION, selection, null, sortOrder); 
       } 
      } else { 
       // group cursor 
       Uri groupsUri = ContactsContract.Groups.CONTENT_SUMMARY_URI; 
       String selection = "((" + ContactsContract.Groups.TITLE 
         + " NOTNULL) AND (" + ContactsContract.Groups.TITLE 
         + " == 'Coworkers') OR (" 
         + ContactsContract.Groups.TITLE 
         + " == 'My Contacts'))"; // Select only Coworkers 
               // (E-mail only) and My 
               // Contacts (Name only) 
       String sortOrder = ContactsContract.Groups.TITLE 
         + " COLLATE LOCALIZED ASC"; 
       cl = new CursorLoader(getApplicationContext(), groupsUri, 
         GROUPS_SUMMARY_PROJECTION, selection, null, sortOrder); 
      } 

      return cl; 
     } 

     @Override 
     public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 
      // Swap the new cursor in. 
      int id = loader.getId(); 
//   Log.d("Dump Cursor MainActivity", 
//     DatabaseUtils.dumpCursorToString(data)); 
      Log.d(DEBUG_TAG, "onLoadFinished() for loader_id " + id); 
      if (id != -1) { 
       // child cursor 
       if (!data.isClosed()) { 
        Log.d(DEBUG_TAG, "data.getCount() " + data.getCount()); 

        HashMap<Integer, Integer> groupMap = listAdapter 
          .getGroupMap(); 
        try { 
         int groupPos = groupMap.get(id); 
         Log.d(DEBUG_TAG, "onLoadFinished() for groupPos " 
           + groupPos); 
         listAdapter.setChildrenCursor(groupPos, data); 
        } catch (NullPointerException e) { 
         Log.w("DEBUG", 
           "Adapter expired, try again on the next query: " 
             + e.getMessage()); 
        } 
       } 
      } else { 
       listAdapter.setGroupCursor(data); 
      } 
     } 

     @Override 
     public void onLoaderReset(Loader<Cursor> loader) { 
      // This is called when the last Cursor provided to onLoadFinished() 
      // is about to be closed. 
      int id = loader.getId(); 
      Log.d(DEBUG_TAG, "onLoaderReset() for loader_id " + id); 
      if (id != 1) { 
       // child cursor 
       try { 
        listAdapter.setChildrenCursor(id, null); 
       } catch (NullPointerException e) { 
        Log.w(DEBUG_TAG, 
          "Adapter expired, try again on the next query: " 
            + e.getMessage()); 
       } 
      } else { 
       listAdapter.setGroupCursor(null); 
      } 
     } 
    }; 

    private ContentObserver mSpeakerChangesObserver = new ContentObserver(
      new Handler()) { 

     @Override 
     public void onChange(boolean selfChange) { 
      if (getApplicationContext() != null) { 
       runOnUiThread(new Runnable() { 
        public void run() { 
         getSupportLoaderManager().restartLoader(-1, null, 
           mSpeakersLoaderCallback); 
        } 
       }); 
      } 
     } 
    }; 
} 

MyListAdapter.java:

package com.example.cursortreeadaptersearch; 

import java.util.HashMap; 

import android.content.Context; 
import android.database.Cursor; 
import android.provider.ContactsContract; 
import android.support.v4.content.Loader; 
import android.util.Log; 
import android.view.LayoutInflater; 
import android.view.View; 
import android.view.ViewGroup; 
import android.widget.CursorTreeAdapter; 
import android.widget.TextView; 

public class MyListAdapter extends CursorTreeAdapter { 

    public HashMap<String, View> childView = new HashMap<String, View>(); 

    /** 
    * The columns we are interested in from the database 
    */ 

    private final String DEBUG_TAG = getClass().getSimpleName().toString(); 

    protected final HashMap<Integer, Integer> mGroupMap; 

    private MainActivity mActivity; 
    private LayoutInflater mInflater; 

    String mConstraint; 

    public MyListAdapter(Cursor cursor, Context context) { 

     super(cursor, context); 
     mActivity = (MainActivity) context; 
     mInflater = LayoutInflater.from(context); 
     mGroupMap = new HashMap<Integer, Integer>(); 
    } 

    @Override 
    public View newGroupView(Context context, Cursor cursor, 
      boolean isExpanded, ViewGroup parent) { 

     final View view = mInflater.inflate(R.layout.list_group, parent, false); 
     return view; 
    } 

    @Override 
    public void bindGroupView(View view, Context context, Cursor cursor, 
      boolean isExpanded) { 

     TextView lblListHeader = (TextView) view 
       .findViewById(R.id.lblListHeader); 

     if (lblListHeader != null) { 
      lblListHeader.setText(cursor.getString(cursor 
        .getColumnIndex(ContactsContract.Groups.TITLE))); 
     } 
    } 

    @Override 
    public View newChildView(Context context, Cursor cursor, 
      boolean isLastChild, ViewGroup parent) { 

     final View view = mInflater.inflate(R.layout.list_item, parent, false); 

     return view; 
    } 

    @Override 
    public void bindChildView(View view, Context context, Cursor cursor, 
      boolean isLastChild) { 

     TextView txtListChild = (TextView) view.findViewById(R.id.lblListItem); 

     if (txtListChild != null) { 
      txtListChild.setText(cursor.getString(1)); // Selects E-mail or 
                 // Display Name 
     } 

    } 

    protected Cursor getChildrenCursor(Cursor groupCursor) { 
     // Given the group, we return a cursor for all the children within that 
     // group 
     int groupPos = groupCursor.getPosition(); 
     int groupId = groupCursor.getInt(groupCursor 
       .getColumnIndex(ContactsContract.Groups._ID)); 

     Log.d(DEBUG_TAG, "getChildrenCursor() for groupPos " + groupPos); 
     Log.d(DEBUG_TAG, "getChildrenCursor() for groupId " + groupId); 

     mGroupMap.put(groupId, groupPos); 

     Loader loader = mActivity.getSupportLoaderManager().getLoader(groupId); 
     if (loader != null && !loader.isReset()) { 
      mActivity.getSupportLoaderManager().restartLoader(groupId, null, 
        mActivity.mSpeakersLoaderCallback); 
     } else { 
      mActivity.getSupportLoaderManager().initLoader(groupId, null, 
        mActivity.mSpeakersLoaderCallback); 
     } 

     return null; 
    } 

    // Access method 
    public HashMap<Integer, Integer> getGroupMap() { 
     return mGroupMap; 
    } 

    public void filterList(CharSequence constraint) { 
     // TODO Filter the data here 
    } 
} 

ho molto notevolmente semplificato e pulito il codice (s o che voi ragazzi non avete bisogno di farlo).

Come potete vedere, ho in totale 3 cursori (1 per i gruppi e 2 per i bambini). I dati sono ottenuti da ContactsContract (che sono i contatti dell'utente). Il cursore da child 1 rappresenta tutte le e-mail di tutti i contatti e il cursore da child 2 rappresenta tutti i nomi visualizzati dei contatti. (La maggior parte delle funzioni del caricatore è da here).

L'unica cosa è ora come implementare una ricerca? Devo farlo tramite Content Provider o una query non elaborata nel database? Mi piacerebbe che venissero visualizzati i risultati di entrambe le tabelle per bambini. Penso che sia facile commettere un errore durante la digitazione che nel mio caso sia tokenize=porter.

Spero che qualcuno possa indicarmi una buona direzione.

Edit:

Ho provato questo in MyListAdapter.java (con FilterQueryProvider come suggerito da Kyle I.):

public void filterList(CharSequence constraint) { 
    final Cursor oldCursor = getCursor(); 
    setFilterQueryProvider(filterQueryProvider); 
    getFilter().filter(constraint, new FilterListener() { 
     public void onFilterComplete(int count) { 
      // assuming your activity manages the Cursor 
      // (which is a recommended way) 
      notifyDataSetChanged(); 
//   stopManagingCursor(oldCursor); 
//   final Cursor newCursor = getCursor(); 
//   startManagingCursor(newCursor); 
//   // safely close the oldCursor 
      if (oldCursor != null && !oldCursor.isClosed()) { 
       oldCursor.close(); 
      } 
     } 
    }); 
} 

private FilterQueryProvider filterQueryProvider = new FilterQueryProvider() { 
    public Cursor runQuery(CharSequence constraint) { 
     // assuming you have your custom DBHelper instance 
     // ready to execute the DB request 
     String s = '%' + constraint.toString() + '%'; 
     return mActivity.getContentResolver().query(ContactsContract.Data.CONTENT_URI, 
       MainActivity.CONTACTS_PROJECTION, 
       ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME + " LIKE ?", 
      new String[] { s }, 
      null); 
    } 
}; 

E questo a MainActivity.java:

 search.setOnQueryTextListener(new OnQueryTextListener() { 

      @Override 
      public boolean onQueryTextSubmit(String query) { 
       listAdapter.filterList(query); 
       expandAll(); 
       return false; 
      } 

      @Override 
      public boolean onQueryTextChange(String query) { 
       listAdapter.filterList(query); 
       expandAll(); 
       return false; 
      } 
     }); 

     search.setOnCloseListener(new OnCloseListener() { 

      @Override 
      public boolean onClose() { 
       listAdapter.filterList(""); 
       expandAll(); 
       return false; 
      } 
     }); 

Ma poi ho questi errori quando provo a cercare:

12-20 13:20:19.449: E/CursorWindow(28747): Failed to read row 0, column -1 from a CursorWindow which has 96 rows, 4 columns. 
12-20 13:20:19.449: D/AndroidRuntime(28747): Shutting down VM 
12-20 13:20:19.449: W/dalvikvm(28747): threadid=1: thread exiting with uncaught exception (group=0x415c62a0) 
12-20 13:20:19.499: E/AndroidRuntime(28747): FATAL EXCEPTION: main 
12-20 13:20:19.499: E/AndroidRuntime(28747): java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it. 

Quello che sto facendo di sbagliato? O è perché sto solo restituire 1 query (nomi visualizzati) invece di 2 (visualizza nomi ed e-mail) in runQuery?

Edit 2:

Prima di tutto ho cambiato tutti i miei implementazioni di database per ContactsContract. Questo è diventato più facile da mantenere in modo da non dover scrivere la propria implementazione del database.

Quello che ho provato ora è di salvare il mio vincolo in runQuery() di FilterQueryProvider, e quindi in getChildrenCursor eseguire una query contro tale vincolo.(Come suggerito da JRaymond)

private String mConstraint; 
protected Cursor getChildrenCursor(Cursor groupCursor) { 
    // Given the group, we return a cursor for all the children within that 
    // group 
    int groupPos = groupCursor.getPosition(); 
    int groupId = groupCursor.getInt(groupCursor 
      .getColumnIndex(ContactsContract.Groups._ID)); 

    Log.d(DEBUG_TAG, "getChildrenCursor() for groupPos " + groupPos); 
    Log.d(DEBUG_TAG, "getChildrenCursor() for groupId " + groupId); 

    mGroupMap.put(groupId, groupPos); 

    Bundle b = new Bundle(); 
    b.putString("constraint", mConstraint); 

    Loader loader = mActivity.getSupportLoaderManager().getLoader(groupId); 
    if (loader != null && !loader.isReset()) { 
     if (mConstraint == null || mConstraint.isEmpty()) { 
      // Normal query 
      mActivity.getSupportLoaderManager().restartLoader(groupId, 
        null, mActivity.mSpeakersLoaderCallback); 
     } else { 
      // Constrained query 
      mActivity.getSupportLoaderManager().restartLoader(groupId, b, 
        mActivity.mSpeakersLoaderCallback); 

     } 
    } else { 
     if (mConstraint == null || mConstraint.isEmpty()) { 
      // Normal query 
      mActivity.getSupportLoaderManager().initLoader(groupId, null, 
        mActivity.mSpeakersLoaderCallback); 
     } else { 
      // Constrained query 
      mActivity.getSupportLoaderManager().initLoader(groupId, b, 
        mActivity.mSpeakersLoaderCallback); 
     } 
    } 

    return null; 
} 

Ed ecco il FilterQueryProvider:

private FilterQueryProvider filterQueryProvider = new FilterQueryProvider() { 
    public Cursor runQuery(CharSequence constraint) { 
     // Load the group cursor here and assign mConstraint 
     mConstraint = constraint.toString(); 
     Uri groupsUri = ContactsContract.Groups.CONTENT_SUMMARY_URI; 
     String selection = "((" + ContactsContract.Groups.TITLE 
       + " NOTNULL) AND (" + ContactsContract.Groups.TITLE 
       + " == 'Coworkers') OR (" + ContactsContract.Groups.TITLE 
       + " == 'My Contacts'))"; // Select only Coworkers 
              // (E-mail only) and My 
              // Contacts (Name only) 
     String sortOrder = ContactsContract.Groups.TITLE 
       + " COLLATE LOCALIZED ASC"; 
     return mActivity.getContentResolver().query(groupsUri, 
       MainActivity.GROUPS_SUMMARY_PROJECTION, selection, null, 
       sortOrder); 
    } 
}; 

Come potete vedere ho caricare la query dei gruppi, al fine di ottenere il getChildrenCursor di lavoro. Solo per la query dovrei essere eseguito in MainActivity che ottengo dal pacchetto?

Il progetto può essere scaricato here, che è possibile importare in Eclipse.

risposta

4

Ho esaminato il problema e purtroppo non ho il tempo di replicare la configurazione. In termini generici, tuttavia, si dovrebbe essere in grado di salvare il vincolo, e poi in 'getChildrenCursor', eseguire una query su quel vincolo:

Cursor getChildrenCursor(Cursor groupCursor) { 
    if (mConstraint == null || mConstraint.isEmpty()) { 
    // Normal query 
    } else { 
    // Constrained query 
    } 

} 

Io non sono certo, ma sono abbastanza sicuro che getChildrenCursor() verrà richiamato in risposta a una modifica del cursore genitore quando si restituisce il cursore in filterQueryProvider(). Quindi gestisci lo stato null/pieno del vincolo.

Dettagli:

Nella funzione filterlist, invece di fare una procedura complicata, basta chiamare runQueryOnBackgroundThread(constraint);. Ciò scaricherà automaticamente il lavoro del database sullo sfondo. Salva il tuo vincolo nel filterQueryProvider:

String s = '%' + constraint.toString() + '%'; 
mConstraint = s; 

per la query, dipende solo da ciò che si sta cercando di uscire dalla banca dati - un rapido adeguamento al codice che hai postato esegue la query in questo modo:

String selection = ContactsContract.CommonDataKinds.Email.DATA 
    + " NOT LIKE ''"; 
if (constraint != null) { 
    selection += " AND " + ContactsContract.CommonDataKinds.Email.DATA + " LIKE ?"; 
} 
cl = new CursorLoader(getApplicationContext(), 
    ContactsContract.CommonDataKinds.Email.CONTENT_URI, 
    PROJECTION, selection, constraint, sortOrder); 

L'unica cosa di cui non sono molto sicuro è la funzione di espansione automatica in corso, Il mio filtro funziona ma è necessario comprimere e aprire di nuovo l'elenco per vedere il cambiamento.

+0

Grazie! Cosa devo fare per la query su MainActivity? E come dovrei implementarlo? Vedi la mia modifica per ulteriori informazioni. A proposito, per il problema di replica che ho ora caricato il mio progetto, puoi scaricarlo [qui] (http://we.tl/ZPgvWlCwVc). (Dovrebbe funzionare se hai contatti sul tuo Android, non ho testato su un emulatore.) – user2784435

+0

Wooah grazie! Puoi caricare il tuo progetto da qualche parte così posso provarlo? Nel mio progetto funziona anche ora con il tuo aiuto, solo che lo faccio con i bundle. (Non so quale sia migliore). Puoi scaricarlo [qui] (http://we.tl/N8TasbIRoT). Per la cosa del collasso hai provato 'listAdapter.notifyDataSetChanged()'? – user2784435

+0

@ user2784435 Ho provato notifyDataSetChanged, ma questo mi ha messo in un ciclo perpetuo di ricaricamento. Posso provare a metterlo da qualche parte ad un certo punto; ma lo sto passando anche tramite il pacchetto - lo salvo appena all'interno della classe prima di avviare il caricatore – JRaymond

2

Quello che dovresti fare è estendere FilterQueryProvider. Ciò fornisce una funzione runQuery() che restituisce un nuovo cursore di risultati filtrati (probabilmente raggiunto con una query di database).

Nell'implementazione dell'adattatore CursorTreeAdapter verrà quindi utilizzato il metodo setFilterQueryProvider() per fornire un'istanza di FilterQueryProvider.

Infine, quando si desidera eseguire il filtraggio si chiama mAdapter.getFilter().filter("c").

Tuttavia, dal momento che non si stanno effettivamente utilizzando le funzioni di completamento automatico SearchView e si inserisce il proprio elenco, la soluzione scelta è un po 'più complicata di quanto non sia necessario. Perché non si rilascia invece Content Provider e CursorTreeAdapter e si utilizza uno schema più semplice in memoria di elenchi o mappe per il retro dell'adattatore? Compila i dati in memoria come richiesto (l'intero set di dati può essere inserito nella memoria?).

+0

Grazie per aver segnalato 'FilterQueryProvider'. La ragione per cui non utilizzo i dati in memoria è perché il set di dati è "grande" per la memoria, ma il tempo di caricamento è troppo lungo. Non ho ancora implementato le funzionalità di completamento automatico di 'SearchView' perché la mia priorità principale è rendere la ricerca funzionante.Ho provato alcune cose con 'FilterQueryProvider' ma ho finito con un errore' Impossibile leggere la riga', vedere la mia modifica. Dovrei usare ['MergeCursor'] (http://developer.android.com/reference/android/database/MergeCursor.html) in questo caso? – user2784435