1 /* 2 * Copyright 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.androidx.slice.demos; 18 19 import static androidx.slice.core.SliceHints.INFINITY; 20 21 import static com.example.androidx.slice.demos.SampleSliceProvider.URI_PATHS; 22 import static com.example.androidx.slice.demos.SampleSliceProvider.getUri; 23 24 import android.content.ContentResolver; 25 import android.content.Intent; 26 import android.content.pm.ActivityInfo; 27 import android.content.pm.PackageInfo; 28 import android.content.pm.PackageManager; 29 import android.database.Cursor; 30 import android.database.MatrixCursor; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.provider.BaseColumns; 34 import android.util.ArrayMap; 35 import android.util.Log; 36 import android.view.Menu; 37 import android.view.MenuItem; 38 import android.view.SubMenu; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.widget.CursorAdapter; 42 import android.widget.SearchView; 43 import android.widget.SimpleCursorAdapter; 44 import android.widget.Toast; 45 46 import androidx.annotation.NonNull; 47 import androidx.appcompat.app.AppCompatActivity; 48 import androidx.appcompat.widget.Toolbar; 49 import androidx.lifecycle.LiveData; 50 import androidx.slice.Slice; 51 import androidx.slice.SliceItem; 52 import androidx.slice.SliceMetadata; 53 import androidx.slice.widget.EventInfo; 54 import androidx.slice.widget.SliceLiveData; 55 import androidx.slice.widget.SliceView; 56 57 import java.util.ArrayList; 58 import java.util.Collections; 59 import java.util.List; 60 61 /** 62 * Example use of SliceView. Uses a search bar to select/auto-complete a slice uri which is 63 * then displayed in the selected mode with SliceView. 64 */ 65 public class SliceBrowser extends AppCompatActivity implements SliceView.OnSliceActionListener { 66 67 private static final String TAG = "SlicePresenter"; 68 69 private static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI"; 70 private static final boolean TEST_INTENT = false; 71 private static final boolean TEST_THEMES = true; 72 private static final boolean SCROLLING_ENABLED = true; 73 74 private ArrayList<Uri> mSliceUris = new ArrayList<Uri>(); 75 private int mSelectedMode; 76 private ViewGroup mContainer; 77 private SearchView mSearchView; 78 private SimpleCursorAdapter mAdapter; 79 private SubMenu mTypeMenu; 80 private LiveData<Slice> mSliceLiveData; 81 82 @Override 83 public void onCreate(Bundle savedInstanceState) { 84 super.onCreate(savedInstanceState); 85 setContentView(R.layout.activity_layout); 86 87 Toolbar toolbar = findViewById(R.id.search_toolbar); 88 setSupportActionBar(toolbar); 89 90 // Shows the slice 91 mContainer = findViewById(R.id.slice_preview); 92 mSearchView = findViewById(R.id.search_view); 93 94 final String[] from = new String[]{"uri"}; 95 final int[] to = new int[]{android.R.id.text1}; 96 mAdapter = new SimpleCursorAdapter(this, R.layout.simple_list_item_1, 97 null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); 98 mSearchView.setSuggestionsAdapter(mAdapter); 99 mSearchView.setIconifiedByDefault(false); 100 mSearchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() { 101 @Override 102 public boolean onSuggestionClick(int position) { 103 mSearchView.setQuery(((Cursor) mAdapter.getItem(position)).getString(1), true); 104 return true; 105 } 106 107 @Override 108 public boolean onSuggestionSelect(int position) { 109 mSearchView.setQuery(((Cursor) mAdapter.getItem(position)).getString(1), true); 110 return true; 111 } 112 }); 113 mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { 114 @Override 115 public boolean onQueryTextSubmit(String s) { 116 addSlice(Uri.parse(s)); 117 mSearchView.clearFocus(); 118 return false; 119 } 120 121 @Override 122 public boolean onQueryTextChange(String s) { 123 populateAdapter(s); 124 return false; 125 } 126 }); 127 128 mSelectedMode = (savedInstanceState != null) 129 ? savedInstanceState.getInt("SELECTED_MODE", SliceView.MODE_LARGE) 130 : SliceView.MODE_LARGE; 131 if (savedInstanceState != null) { 132 mSearchView.setQuery(savedInstanceState.getString("SELECTED_QUERY"), true); 133 } 134 135 // TODO: Listen for changes. 136 updateAvailableSlices(); 137 if (TEST_INTENT) { 138 addSlice(new Intent("androidx.intent.SLICE_ACTION").setPackage(getPackageName())); 139 } 140 } 141 142 @Override 143 public boolean onCreateOptionsMenu(Menu menu) { 144 mTypeMenu = menu.addSubMenu("Type"); 145 mTypeMenu.setIcon(R.drawable.ic_large); 146 mTypeMenu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 147 mTypeMenu.add("Shortcut"); 148 mTypeMenu.add("Small"); 149 mTypeMenu.add("Large"); 150 super.onCreateOptionsMenu(menu); 151 return true; 152 } 153 154 @Override 155 public boolean onOptionsItemSelected(MenuItem item) { 156 switch (item.getTitle().toString()) { 157 case "Shortcut": 158 mTypeMenu.setIcon(R.drawable.ic_shortcut); 159 mSelectedMode = SliceView.MODE_SHORTCUT; 160 updateSliceModes(); 161 return true; 162 case "Small": 163 mTypeMenu.setIcon(R.drawable.ic_small); 164 mSelectedMode = SliceView.MODE_SMALL; 165 updateSliceModes(); 166 return true; 167 case "Large": 168 mTypeMenu.setIcon(R.drawable.ic_large); 169 mSelectedMode = SliceView.MODE_LARGE; 170 updateSliceModes(); 171 return true; 172 } 173 return super.onOptionsItemSelected(item); 174 } 175 176 @Override 177 protected void onSaveInstanceState(Bundle outState) { 178 super.onSaveInstanceState(outState); 179 outState.putInt("SELECTED_MODE", mSelectedMode); 180 outState.putString("SELECTED_QUERY", mSearchView.getQuery().toString()); 181 } 182 183 private void updateAvailableSlices() { 184 mSliceUris.clear(); 185 List<PackageInfo> packageInfos = getPackageManager() 186 .getInstalledPackages(PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA); 187 for (PackageInfo pi : packageInfos) { 188 ActivityInfo[] activityInfos = pi.activities; 189 if (activityInfos != null) { 190 for (ActivityInfo ai : activityInfos) { 191 if (ai.metaData != null) { 192 String sliceUri = ai.metaData.getString(SLICE_METADATA_KEY); 193 if (sliceUri != null) { 194 mSliceUris.add(Uri.parse(sliceUri)); 195 } 196 } 197 } 198 } 199 } 200 for (int i = 0; i < URI_PATHS.length; i++) { 201 mSliceUris.add(getUri(URI_PATHS[i], getApplicationContext())); 202 } 203 populateAdapter(String.valueOf(mSearchView.getQuery())); 204 } 205 206 private void addSlice(Intent intent) { 207 SliceView v = createSliceView(); 208 v.setTag(intent); 209 mContainer.removeAllViews(); 210 mContainer.addView(v); 211 mSliceLiveData = SliceLiveData.fromIntent(this, intent); 212 v.setMode(mSelectedMode); 213 mSliceLiveData.observe(this, v); 214 } 215 216 private void addSlice(Uri uri) { 217 if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { 218 SliceView v = createSliceView(); 219 v.setTag(uri); 220 mContainer.removeAllViews(); 221 mContainer.addView(v); 222 mSliceLiveData = SliceLiveData.fromUri(this, uri); 223 v.setMode(mSelectedMode); 224 mSliceLiveData.observe(this, slice -> { 225 v.setSlice(slice); 226 SliceMetadata metadata = SliceMetadata.from(this, slice); 227 long expiry = metadata.getExpiry(); 228 if (expiry != INFINITY) { 229 // Shows the updated text after the TTL expires. 230 v.postDelayed(() -> v.setSlice(slice), 231 expiry - System.currentTimeMillis() + 15); 232 } 233 }); 234 mSliceLiveData.observe(this, slice -> Log.d(TAG, "Slice: " + slice)); 235 } else { 236 Log.w(TAG, "Invalid uri, skipping slice: " + uri); 237 } 238 } 239 240 private void updateSliceModes() { 241 final int count = mContainer.getChildCount(); 242 for (int i = 0; i < count; i++) { 243 ((SliceView) mContainer.getChildAt(i)).setMode(mSelectedMode); 244 } 245 } 246 247 private void populateAdapter(String query) { 248 final MatrixCursor c = new MatrixCursor(new String[]{BaseColumns._ID, "uri"}); 249 ArrayMap<String, Integer> ranking = new ArrayMap<>(); 250 ArrayList<String> suggestions = new ArrayList(); 251 for (Uri uri : mSliceUris) { 252 253 String uriString = uri.toString(); 254 if (uriString.contains(query)) { 255 ranking.put(uriString, uriString.indexOf(query)); 256 suggestions.add(uriString); 257 } 258 } 259 Collections.sort(suggestions, (o1, o2) -> 260 Integer.compare(ranking.get(o1), ranking.get(o2))); 261 for (int i = 0; i < suggestions.size(); i++) { 262 c.addRow(new Object[]{i, suggestions.get(i)}); 263 } 264 mAdapter.changeCursor(c); 265 } 266 267 @Override 268 public void onSliceAction(@NonNull EventInfo info, @NonNull SliceItem item) { 269 Log.w(TAG, "onSliceAction, info: " + info); 270 Log.w(TAG, "onSliceAction, sliceItem: \n" + item); 271 } 272 273 private SliceView createSliceView() { 274 SliceView v = TEST_THEMES 275 ? new SliceView(this) 276 : new SliceView(getApplicationContext()); 277 v.setOnSliceActionListener(this); 278 v.setOnClickListener(new View.OnClickListener() { 279 @Override 280 public void onClick(View v) { 281 Toast.makeText(getApplicationContext(), 282 "Custom listener clicked", Toast.LENGTH_SHORT).show(); 283 } 284 }); 285 if (mSliceLiveData != null) { 286 mSliceLiveData.removeObservers(this); 287 } 288 v.setScrollable(SCROLLING_ENABLED); 289 v.setOnLongClickListener(new View.OnLongClickListener() { 290 @Override 291 public boolean onLongClick(View v) { 292 Toast.makeText(getApplicationContext(), "LONGPRESS !!", Toast.LENGTH_SHORT).show(); 293 return true; 294 } 295 }); 296 return v; 297 } 298 } 299