1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.settings.search; 16 17 import static android.app.slice.Slice.HINT_LARGE; 18 import static android.app.slice.Slice.HINT_TITLE; 19 import static android.app.slice.SliceItem.FORMAT_TEXT; 20 import static com.android.settings.search.DeviceIndexFeatureProvider.createDeepLink; 21 22 import android.app.job.JobParameters; 23 import android.app.job.JobService; 24 import android.content.ContentResolver; 25 import android.content.Intent; 26 import android.net.Uri; 27 import android.net.Uri.Builder; 28 import android.provider.SettingsSlicesContract; 29 import android.util.Log; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.settings.overlay.FeatureFactory; 33 import com.android.settings.slices.SettingsSliceProvider; 34 import com.android.settings.slices.SliceDeepLinkSpringBoard; 35 36 import java.util.Collection; 37 import java.util.concurrent.CountDownLatch; 38 39 import androidx.slice.Slice; 40 import androidx.slice.SliceItem; 41 import androidx.slice.SliceViewManager; 42 import androidx.slice.SliceViewManager.SliceCallback; 43 import androidx.slice.SliceMetadata; 44 import androidx.slice.core.SliceQuery; 45 import androidx.slice.widget.ListContent; 46 47 public class DeviceIndexUpdateJobService extends JobService { 48 49 private static final String TAG = "DeviceIndexUpdate"; 50 private static final boolean DEBUG = false; 51 @VisibleForTesting 52 protected boolean mRunningJob; 53 54 @Override 55 public boolean onStartJob(JobParameters params) { 56 if (DEBUG) Log.d(TAG, "onStartJob"); 57 if (!mRunningJob) { 58 mRunningJob = true; 59 Thread thread = new Thread(() -> updateIndex(params)); 60 thread.setPriority(Thread.MIN_PRIORITY); 61 thread.start(); 62 } 63 return true; 64 } 65 66 @Override 67 public boolean onStopJob(JobParameters params) { 68 if (DEBUG) Log.d(TAG, "onStopJob " + mRunningJob); 69 if (mRunningJob) { 70 mRunningJob = false; 71 return true; 72 } 73 return false; 74 } 75 76 @VisibleForTesting 77 protected void updateIndex(JobParameters params) { 78 if (DEBUG) { 79 Log.d(TAG, "Starting index"); 80 } 81 final DeviceIndexFeatureProvider indexProvider = FeatureFactory.getFactory(this) 82 .getDeviceIndexFeatureProvider(); 83 final SliceViewManager manager = getSliceViewManager(); 84 final Uri baseUri = new Builder() 85 .scheme(ContentResolver.SCHEME_CONTENT) 86 .authority(SettingsSliceProvider.SLICE_AUTHORITY) 87 .build(); 88 final Uri platformBaseUri = new Builder() 89 .scheme(ContentResolver.SCHEME_CONTENT) 90 .authority(SettingsSlicesContract.AUTHORITY) 91 .build(); 92 final Collection<Uri> slices = manager.getSliceDescendants(baseUri); 93 slices.addAll(manager.getSliceDescendants(platformBaseUri)); 94 95 if (DEBUG) { 96 Log.d(TAG, "Indexing " + slices.size() + " slices"); 97 } 98 99 indexProvider.clearIndex(this /* context */); 100 101 for (Uri slice : slices) { 102 if (!mRunningJob) { 103 return; 104 } 105 Slice loadedSlice = bindSliceSynchronous(manager, slice); 106 // TODO: Get Title APIs on SliceMetadata and use that. 107 SliceMetadata metaData = getMetadata(loadedSlice); 108 CharSequence title = findTitle(loadedSlice, metaData); 109 if (title != null) { 110 if (DEBUG) { 111 Log.d(TAG, "Indexing: " + slice + " " + title + " " + loadedSlice); 112 } 113 indexProvider.index(this, title, slice, createDeepLink( 114 new Intent(SliceDeepLinkSpringBoard.ACTION_VIEW_SLICE) 115 .setPackage(getPackageName()) 116 .putExtra(SliceDeepLinkSpringBoard.EXTRA_SLICE, slice.toString()) 117 .toUri(Intent.URI_ANDROID_APP_SCHEME)), 118 metaData.getSliceKeywords()); 119 } 120 } 121 if (DEBUG) { 122 Log.d(TAG, "Done indexing"); 123 } 124 jobFinished(params, false); 125 } 126 127 protected SliceViewManager getSliceViewManager() { 128 return SliceViewManager.getInstance(this); 129 } 130 131 protected SliceMetadata getMetadata(Slice loadedSlice) { 132 return SliceMetadata.from(this, loadedSlice); 133 } 134 135 protected CharSequence findTitle(Slice loadedSlice, SliceMetadata metaData) { 136 ListContent content = new ListContent(null, loadedSlice); 137 SliceItem headerItem = content.getHeaderItem(); 138 if (headerItem == null) { 139 if (content.getRowItems().size() != 0) { 140 headerItem = content.getRowItems().get(0); 141 } else { 142 return null; 143 } 144 } 145 // Look for a title, then large text, then any text at all. 146 SliceItem title = SliceQuery.find(headerItem, FORMAT_TEXT, HINT_TITLE, null); 147 if (title != null) { 148 return title.getText(); 149 } 150 title = SliceQuery.find(headerItem, FORMAT_TEXT, HINT_LARGE, null); 151 if (title != null) { 152 return title.getText(); 153 } 154 title = SliceQuery.find(headerItem, FORMAT_TEXT); 155 if (title != null) { 156 return title.getText(); 157 } 158 return null; 159 } 160 161 protected Slice bindSliceSynchronous(SliceViewManager manager, Uri slice) { 162 final Slice[] returnSlice = new Slice[1]; 163 CountDownLatch latch = new CountDownLatch(1); 164 SliceCallback callback = new SliceCallback() { 165 @Override 166 public void onSliceUpdated(Slice s) { 167 try { 168 SliceMetadata m = SliceMetadata.from(DeviceIndexUpdateJobService.this, s); 169 if (m.getLoadingState() == SliceMetadata.LOADED_ALL) { 170 returnSlice[0] = s; 171 latch.countDown(); 172 manager.unregisterSliceCallback(slice, this); 173 } 174 } catch (Exception e) { 175 Log.w(TAG, slice + " cannot be indexed", e); 176 returnSlice[0] = s; 177 } 178 } 179 }; 180 // Register a callback until we get a loaded slice. 181 manager.registerSliceCallback(slice, callback); 182 // Trigger the first bind in case no loading is needed. 183 callback.onSliceUpdated(manager.bindSlice(slice)); 184 try { 185 latch.await(); 186 } catch (InterruptedException e) { 187 } 188 return returnSlice[0]; 189 } 190 } 191